├── src ├── spec │ ├── spec-helper.coffee │ ├── tag-names.coffee │ ├── regions.coffee │ ├── namespace.coffee │ ├── fixtures.coffee │ ├── static.coffee │ ├── root.coffee │ ├── images.coffee │ ├── videos.coffee │ └── text.coffee ├── sandbox │ ├── sandbox.coffee │ └── sandbox.scss ├── scripts │ ├── tag-names.coffee │ ├── fixtures.coffee │ ├── regions.coffee │ ├── static.coffee │ ├── videos.coffee │ ├── namespace.coffee │ ├── root.coffee │ ├── images.coffee │ ├── tables.coffee │ └── lists.coffee └── styles │ └── content-edit.scss ├── .gitignore ├── spec └── spec-helper.js ├── sandbox ├── example.png ├── sandbox.css ├── sandbox.js └── index.html ├── .travis.yml ├── package.json ├── SpecRunner.html ├── bower.json ├── LICENSE ├── README.md ├── Gruntfile.coffee └── external └── content-select.js /src/spec/spec-helper.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | jasmine 3 | node_modules -------------------------------------------------------------------------------- /spec/spec-helper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 4 | }).call(this); 5 | -------------------------------------------------------------------------------- /sandbox/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cubiclesoft/ContentEdit/master/sandbox/example.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /src/sandbox/sandbox.coffee: -------------------------------------------------------------------------------- 1 | window.onload = () -> 2 | 3 | regions = document.querySelectorAll('.edit-me') 4 | for region in regions 5 | new ContentEdit.Region(region) 6 | 7 | fixtures = document.querySelectorAll('.fixture') 8 | for fixture in fixtures 9 | new ContentEdit.Fixture(fixture) -------------------------------------------------------------------------------- /sandbox/sandbox.css: -------------------------------------------------------------------------------- 1 | /*! ContentEdit v1.2.1 by Anthony Blackshaw (https://github.com/anthonyjb) */ 2 | body{color:#22313F;font-family:Arial, sans-serif;margin:0;padding:20px 0 0}.edit-me,.fixture{margin:auto;width:960px}table{border-collapse:collapse;margin-top:20px;table-layout:fixed;width:100%}caption{font-style:italic}th{border-bottom:1px solid #22313F;font-weight:bold;padding:10px 0;text-align:left}td{border-bottom:1px solid #ccc;padding:10px 0}tr,td,th{vertical-align:middle}pre{background:#eee;padding:10px}.image-fixture{background:#eee;background-size:cover;border-radius:100px;height:200px;margin:20px auto;width:200px} 3 | -------------------------------------------------------------------------------- /sandbox/sandbox.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | window.onload = function() { 3 | var fixture, fixtures, region, regions, _i, _j, _len, _len1, _results; 4 | regions = document.querySelectorAll('.edit-me'); 5 | for (_i = 0, _len = regions.length; _i < _len; _i++) { 6 | region = regions[_i]; 7 | new ContentEdit.Region(region); 8 | } 9 | fixtures = document.querySelectorAll('.fixture'); 10 | _results = []; 11 | for (_j = 0, _len1 = fixtures.length; _j < _len1; _j++) { 12 | fixture = fixtures[_j]; 13 | _results.push(new ContentEdit.Fixture(fixture)); 14 | } 15 | return _results; 16 | }; 17 | 18 | }).call(this); 19 | -------------------------------------------------------------------------------- /src/sandbox/sandbox.scss: -------------------------------------------------------------------------------- 1 | body { 2 | color: #22313F; 3 | font-family: Arial, sans-serif; 4 | margin: 0; 5 | padding: 20px 0 0; 6 | } 7 | 8 | .edit-me, .fixture { 9 | margin: auto; 10 | width: 960px; 11 | } 12 | 13 | table { 14 | border-collapse: collapse; 15 | margin-top: 20px; 16 | table-layout: fixed; 17 | width: 100%; 18 | } 19 | 20 | caption { 21 | font-style: italic; 22 | } 23 | 24 | th { 25 | border-bottom: 1px solid #22313F; 26 | font-weight: bold; 27 | padding: floor(20px / 2) 0; 28 | text-align: left; 29 | } 30 | 31 | td { 32 | border-bottom: 1px solid #ccc; 33 | padding: floor(20px / 2) 0; 34 | } 35 | 36 | tr, 37 | td, 38 | th { 39 | vertical-align: middle; 40 | } 41 | 42 | pre { 43 | background: #eee; 44 | padding: 10px; 45 | } 46 | 47 | .image-fixture { 48 | background: #eee; 49 | background-size: cover; 50 | border-radius: 100px; 51 | height: 200px; 52 | margin: 20px auto; 53 | width: 200px; 54 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContentEdit", 3 | "description": "A JavaScript library that provides a set of classes for building content-editable HTML elements.", 4 | "version": "1.3.1", 5 | "keywords": [ 6 | "wysiwyg", 7 | "inline", 8 | "html", 9 | "editor" 10 | ], 11 | "author": { 12 | "name": "Anthony Blackshaw", 13 | "email": "ant@getme.co.uk", 14 | "url": "https://github.com/anthonyjb" 15 | }, 16 | "main": "build/content-edit.js", 17 | "devDependencies": { 18 | "grunt": "~0.4.5", 19 | "grunt-contrib-clean": "^0.6.0", 20 | "grunt-contrib-coffee": "^0.11.1", 21 | "grunt-contrib-concat": "^0.5.0", 22 | "grunt-contrib-jasmine": "^0.9.2", 23 | "grunt-contrib-sass": "^0.8.1", 24 | "grunt-contrib-uglify": "^0.7.0", 25 | "grunt-contrib-watch": "^0.6.1" 26 | }, 27 | "scripts": { 28 | "test": "grunt jasmine --verbose" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/GetmeUK/ContentEdit.git" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ContentEdit spec runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContentEdit", 3 | "description": "A JavaScript library that provides a set of classes for building content-editable HTML elements.", 4 | "main": [ 5 | "build/content-edit.js", 6 | "build/content-edit.min.css", 7 | "build/images/drop-horz.svg", 8 | "build/images/drop-vert-above.svg", 9 | "build/images/drop-vert-below.svg", 10 | "build/images/video.svg", 11 | "src/styles/content-edit.scss" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Anthony Blackshaw", 16 | "email": "ant@getme.co.uk", 17 | "url": "https://github.com/anthonyjb" 18 | } 19 | ], 20 | "license": "MIT", 21 | "keywords": [ 22 | "wysiwyg", 23 | "inline", 24 | "html", 25 | "editor" 26 | ], 27 | "homepage": "http://getcontenttools.com/api/content-edit", 28 | "repository": { 29 | "type": "git", 30 | "url": "git@github.com:GetmeUK/ContentEdit.git" 31 | }, 32 | "moduleType": [ 33 | "globals" 34 | ], 35 | "ignore": [ 36 | "**/.*", 37 | "node_modules", 38 | "bower_components", 39 | "test", 40 | "tests" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Getme Limited (http://getme.co.uk) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/spec/tag-names.coffee: -------------------------------------------------------------------------------- 1 | # TagNames 2 | 3 | describe '`ContentEdit.TagNames.get()`', () -> 4 | 5 | it 'should return a singleton instance of TagNames`', () -> 6 | tagNames = new ContentEdit.TagNames.get() 7 | 8 | # Check the instance returned is a singleton 9 | expect(tagNames).toBe ContentEdit.TagNames.get() 10 | 11 | 12 | describe '`ContentEdit.TagNames.register()`', () -> 13 | 14 | it 'should register a class with one or more tag names', () -> 15 | tagNames = new ContentEdit.TagNames.get() 16 | 17 | # Register some classes to tag names 18 | tagNames.register(ContentEdit.Node, 'foo') 19 | tagNames.register(ContentEdit.Element, 'bar', 'zee') 20 | 21 | expect(tagNames.match('foo')).toBe ContentEdit.Node 22 | expect(tagNames.match('bar')).toBe ContentEdit.Element 23 | expect(tagNames.match('zee')).toBe ContentEdit.Element 24 | 25 | 26 | describe '`ContentEdit.TagNames.match()`', () -> 27 | 28 | tagNames = new ContentEdit.TagNames.get() 29 | 30 | it 'should return a class registered for the specifed tag name', () -> 31 | expect(tagNames.match('img')).toBe ContentEdit.Image 32 | 33 | it 'should return `ContentEdit.Static` if no match is found for the tag 34 | name', () -> 35 | expect(tagNames.match('bom')).toBe ContentEdit.Static -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ContentEdit 2 | 3 | [![Build Status](https://travis-ci.org/GetmeUK/ContentEdit.svg?branch=master)](https://travis-ci.org/GetmeUK/ContentEdit) 4 | 5 | > A JavaScript library that provides a set of classes for building content-editable HTML elements. 6 | 7 | ## Install 8 | 9 | **Using bower** 10 | 11 | ``` 12 | bower install --save ContentEdit 13 | ``` 14 | 15 | **Using npm** 16 | 17 | ``` 18 | npm install --save ContentEdit 19 | ``` 20 | 21 | ## Building 22 | To build the library you'll need to use Grunt. First install the required node modules ([grunt-cli](http://gruntjs.com/getting-started) must be installed): 23 | ``` 24 | git clone https://github.com/GetmeUK/ContentEdit.git 25 | cd ContentEdit 26 | npm install 27 | ``` 28 | 29 | Install Sass (if not already installed): 30 | ``` 31 | gem install sass 32 | ``` 33 | 34 | Then run `grunt build` to build the project. 35 | 36 | ## Testing 37 | To test the library you'll need to use Jasmine. First install Jasmine: 38 | ``` 39 | git clone https://github.com/pivotal/jasmine.git 40 | mkdir ContentEdit/jasmine 41 | mv jasmine/dist/jasmine-standalone-2.0.3.zip ContentEdit/jasmine 42 | cd ContentEdit/jasmine 43 | unzip jasmine-standalone-2.0.3.zip 44 | ``` 45 | 46 | Then open `ContentEdit/SpecRunner.html` in a browser to run the tests. 47 | 48 | Alternatively you can use `grunt jasmine` to run the tests from the command line. 49 | 50 | ## Documentation 51 | Full documentation is available at http://getcontenttools.com/api/content-edit 52 | 53 | ## Browser support 54 | 55 | - Chrome 56 | - Firefox 57 | - IE9+ 58 | -------------------------------------------------------------------------------- /src/scripts/tag-names.coffee: -------------------------------------------------------------------------------- 1 | class _TagNames 2 | 3 | # The `_TagNames` class allows DOM element tag names to be associated with 4 | # `ContentEdit.Element` classes. When a region is initialized it uses this 5 | # association to determine how best to handle each DOM element child. 6 | # 7 | # DOM element tag names are associated with element classes through the 8 | # `register()` method. To handle cases where the same tag name is used for 9 | # more than one element class the `data-ce-tag` attribute can be used to 10 | # specify a tag name. This is also useful when you want to specify a tag 11 | # name that isn't valid in HTML (e.g ... could be specified as 12 | #

...

). 13 | 14 | constructor: () -> 15 | # Map of tag names and their associated element classes 16 | @_tagNames = {} 17 | 18 | register: (cls, tagNames...) -> 19 | # Register an element class with one or more tag names 20 | for tagName in tagNames 21 | @_tagNames[tagName.toLowerCase()] = cls 22 | 23 | match: (tagName) -> 24 | # Return an element class for the specified tag name (case insensitive), 25 | # if we can't find an associated class return `ContentEdit.Static`. 26 | tagName = tagName.toLowerCase() 27 | if @_tagNames[tagName] 28 | return @_tagNames[tagName] 29 | 30 | return ContentEdit.Static 31 | 32 | 33 | class ContentEdit.TagNames 34 | 35 | # The `ContentEdit.TagNames` class is a singleton, this code provides access 36 | # to the singleton instance of the protected `_TagNames` class which is 37 | # initialized the first time the class method `get` is called. 38 | 39 | instance = null 40 | 41 | @get: () -> 42 | instance ?= new _TagNames() -------------------------------------------------------------------------------- /src/scripts/fixtures.coffee: -------------------------------------------------------------------------------- 1 | class ContentEdit.Fixture extends ContentEdit.NodeCollection 2 | 3 | # Fixtures take a DOM element and convert it to a single editable element, 4 | # this allows the creation of field like regions within a page. 5 | 6 | constructor: (domElement) -> 7 | super() 8 | 9 | # The DOM element associated with this region of editable content 10 | @_domElement = domElement 11 | 12 | # Convert the existing contents of the DOM element to editable elements 13 | tagNames = ContentEdit.TagNames.get() 14 | 15 | # Find the class associated with this fixtures tag name 16 | if @_domElement.getAttribute("data-ce-tag") 17 | cls = tagNames.match(@_domElement.getAttribute("data-ce-tag")) 18 | else 19 | cls = tagNames.match(@_domElement.tagName) 20 | 21 | # Convert the node to a ContentEdit.Element 22 | element = cls.fromDOMElement(@_domElement) 23 | 24 | # Modify the mount method for the element 25 | @children = [element] 26 | 27 | element._parent = this 28 | element.mount() 29 | 30 | # Trigger a ready event for the region 31 | ContentEdit.Root.get().trigger('ready', this) 32 | 33 | # Read-only properties 34 | 35 | domElement: () -> 36 | # Return the DOM element associated with the region. 37 | return @_domElement 38 | 39 | isMounted: () -> 40 | # Return true if the node is mounted in the DOM. 41 | return true 42 | 43 | type: () -> 44 | # Return the type of element (this should be the same as the class name) 45 | return 'Fixture' 46 | 47 | # Methods 48 | 49 | html: (indent='') -> 50 | # Return a HTML string for the node 51 | le = ContentEdit.LINE_ENDINGS 52 | return (c.html(indent) for c in @children).join(le).trim() -------------------------------------------------------------------------------- /src/spec/regions.coffee: -------------------------------------------------------------------------------- 1 | # Region 2 | 3 | describe '`ContentEdit.Region()`', () -> 4 | 5 | it 'should return an instance of Region`', () -> 6 | region = new ContentEdit.Region(document.createElement('div')) 7 | expect(region instanceof ContentEdit.Region).toBe true 8 | 9 | 10 | describe '`ContentEdit.Region.domElement()`', () -> 11 | 12 | it 'should return a clone of the DOM element the region was initialized with 13 | or a clone of', () -> 14 | domElement = document.createElement('div') 15 | region = new ContentEdit.Region(domElement) 16 | expect(region.domElement()).toEqual domElement 17 | 18 | 19 | describe '`ContentEdit.Region.isMounted()`', () -> 20 | 21 | it 'should always return true', () -> 22 | region = new ContentEdit.Region(document.createElement('div')) 23 | expect(region.isMounted()).toBe true 24 | 25 | 26 | describe '`ContentEdit.Region.type()`', () -> 27 | 28 | it 'should return \'Region\'', () -> 29 | region = new ContentEdit.Region(document.createElement('div')) 30 | expect(region.type()).toBe 'Region' 31 | 32 | 33 | describe '`ContentEdit.Region.html()`', () -> 34 | 35 | it 'should return a HTML string for the region', () -> 36 | region = new ContentEdit.Region(document.createElement('div')) 37 | 38 | # Add a set of elements to the region 39 | region.attach(new ContentEdit.Text('p', {}, 'one')) 40 | region.attach(new ContentEdit.Text('p', {}, 'two')) 41 | region.attach(new ContentEdit.Text('p', {}, 'three')) 42 | 43 | expect(region.html()).toBe( 44 | '

\n' + 45 | "#{ ContentEdit.INDENT }one\n" + 46 | '

\n' + 47 | '

\n' + 48 | "#{ ContentEdit.INDENT }two\n" + 49 | '

\n' + 50 | '

\n' + 51 | "#{ ContentEdit.INDENT }three\n" + 52 | '

' 53 | ) 54 | 55 | describe '`ContentEdit.Region.setContent()`', () -> 56 | 57 | it 'should set content for the region', () -> 58 | region = new ContentEdit.Region(document.createElement('div')) 59 | 60 | # Build the DOM content 61 | domContent = document.createElement('div') 62 | domContent.innerHTML = '

test with DOM

' 63 | 64 | # Build the HTML Content 65 | htmlContent = '

test with HTML

' 66 | 67 | # Set the content using a DOM element 68 | region.setContent(domContent) 69 | 70 | expect(region.html()).toBe( 71 | '

\n' + 72 | "#{ ContentEdit.INDENT }test with DOM\n" + 73 | '

' 74 | ) 75 | 76 | # Set the content using a HTML string 77 | region.setContent(htmlContent) 78 | 79 | expect(region.html()).toBe( 80 | '

\n' + 81 | "#{ ContentEdit.INDENT }test with HTML\n" + 82 | '

' 83 | ) -------------------------------------------------------------------------------- /src/scripts/regions.coffee: -------------------------------------------------------------------------------- 1 | class ContentEdit.Region extends ContentEdit.NodeCollection 2 | 3 | # Regions take a DOM element and convert the child DOM elements to other 4 | # editable elements. Regions acts as a root collection of the editable 5 | # elements. 6 | 7 | constructor: (domElement) -> 8 | super() 9 | 10 | # The DOM element associated with this region of editable content 11 | @_domElement = domElement 12 | 13 | # Set the content for the region to match the DOM element 14 | @setContent(domElement) 15 | 16 | # Read-only properties 17 | 18 | domElement: () -> 19 | # Return the DOM element associated with the region. 20 | return @_domElement 21 | 22 | isMounted: () -> 23 | # Return true if the node is mounted in the DOM. 24 | return true 25 | 26 | type: () -> 27 | # Return the type of element (this should be the same as the class name) 28 | return 'Region' 29 | 30 | # Methods 31 | 32 | html: (indent='') -> 33 | # Return a HTML string for the node 34 | le = ContentEdit.LINE_ENDINGS 35 | return (c.html(indent) for c in @children).join(le).trim() 36 | 37 | setContent: (domElementOrHTML) -> 38 | # Set the contents of the region using a DOM element or HTML string 39 | domElement = domElementOrHTML 40 | if domElementOrHTML.childNodes == undefined 41 | 42 | # Convert the HTML string to DOM elements we can pass 43 | wrapper = document.createElement('div') 44 | wrapper.innerHTML = domElementOrHTML 45 | domElement = wrapper 46 | 47 | # Unattach any existing elements 48 | for child in @children.slice() 49 | @detach(child) 50 | 51 | # Build and attach new content 52 | 53 | # Convert the existing contents of the DOM element to editable elements 54 | tagNames = ContentEdit.TagNames.get() 55 | 56 | # Create a list if child nodes we can safely remove whilst iterating 57 | # through them. 58 | childNodes = (c for c in domElement.childNodes) 59 | 60 | for childNode in childNodes 61 | 62 | # Filter out non-elements 63 | unless childNode.nodeType == 1 # ELEMENT_NODE 64 | continue 65 | 66 | # Find the class associated with this node's tag name 67 | if childNode.getAttribute('data-ce-tag') 68 | cls = tagNames.match(childNode.getAttribute('data-ce-tag')) 69 | else 70 | cls = tagNames.match(childNode.tagName) 71 | 72 | # Convert the node to a ContentEdit.Element 73 | element = cls.fromDOMElement(childNode) 74 | 75 | # Remove the node from the DOM 76 | domElement.removeChild(childNode) 77 | 78 | # Attach the element to the region 79 | if element 80 | @attach(element) 81 | 82 | # Trigger a ready event for the region 83 | ContentEdit.Root.get().trigger('ready', this) -------------------------------------------------------------------------------- /src/spec/namespace.coffee: -------------------------------------------------------------------------------- 1 | # Namespace (namespace.coffee) 2 | 3 | describe 'ContentEdit', () -> 4 | 5 | it 'should have correct default settings', () -> 6 | 7 | expect(ContentEdit.DEFAULT_MAX_ELEMENT_WIDTH).toBe 800 8 | expect(ContentEdit.DEFAULT_MIN_ELEMENT_WIDTH).toBe 80 9 | expect(ContentEdit.DRAG_HOLD_DURATION).toBe 500 10 | expect(ContentEdit.DROP_EDGE_SIZE).toBe 50 11 | expect(ContentEdit.HELPER_CHAR_LIMIT).toBe 250 12 | expect(ContentEdit.INDENT).toBe ' ' 13 | expect(ContentEdit.LINE_ENDINGS).toBe '\n' 14 | expect(ContentEdit.LANGUAGE).toBe 'en' 15 | expect(ContentEdit.RESIZE_CORNER_SIZE).toBe 15 16 | expect(ContentEdit.TRIM_WHITESPACE).toBe true 17 | 18 | 19 | describe 'ContentEdit._', () -> 20 | 21 | # Note: This covers testing the `addTranslations` method also 22 | 23 | it 'should return a translated string for the current language', () -> 24 | # Add translations for French 25 | ContentEdit.addTranslations('fr', {'hello': 'bonjour'}) 26 | ContentEdit.addTranslations('de', {'hello': 'hallo'}) 27 | 28 | # Check that the English translation is returned by default 29 | expect(ContentEdit._('hello')).toBe 'hello' 30 | 31 | # Check that the French translation is returned when the current 32 | # language is switched to 'fr'. 33 | ContentEdit.LANGUAGE = 'fr' 34 | expect(ContentEdit._('hello')).toBe 'bonjour' 35 | 36 | ContentEdit.LANGUAGE = 'de' 37 | expect(ContentEdit._('hello')).toBe 'hallo' 38 | 39 | # Check that a non translated string is returned as is 40 | expect(ContentEdit._('goodbye')).toBe 'goodbye' 41 | 42 | ContentEdit.LANGUAGE = 'en' 43 | 44 | 45 | describe 'ContentEdit.addCSSClass()', () -> 46 | 47 | it 'should add a CSS class to a DOM element', () -> 48 | # Create a DOM element to test against 49 | domElement = document.createElement('div') 50 | 51 | # Check a single class 52 | ContentEdit.addCSSClass(domElement, 'foo') 53 | expect(domElement.getAttribute('class')).toBe 'foo' 54 | 55 | # Check multiple classes 56 | ContentEdit.addCSSClass(domElement, 'bar') 57 | expect(domElement.getAttribute('class')).toBe 'foo bar' 58 | 59 | 60 | describe 'ContentEdit.attributesToString()', () -> 61 | 62 | it 'should convert a dictionary into a key="value" string', () -> 63 | attributes = { 64 | 'id': 'foo', 65 | 'class': 'bar' 66 | } 67 | string = ContentEdit.attributesToString(attributes) 68 | expect(string).toBe 'class="bar" id="foo"' 69 | 70 | 71 | describe 'ContentEdit.removeCSSClass()', () -> 72 | 73 | it 'should remove a CSS class from a DOM element', () -> 74 | # Create a DOM element to test against 75 | domElement = document.createElement('div') 76 | ContentEdit.addCSSClass(domElement, 'foo') 77 | ContentEdit.addCSSClass(domElement, 'bar') 78 | 79 | # Remove a class 80 | ContentEdit.removeCSSClass(domElement, 'foo') 81 | expect(domElement.getAttribute('class')).toBe 'bar' 82 | 83 | # Remove another class (class should now be null) 84 | ContentEdit.removeCSSClass(domElement, 'bar') 85 | expect(domElement.getAttribute('class')).toBe null 86 | 87 | it 'should do nothing if the CSS class being removed is not set against the DOM element', () -> 88 | # Create a DOM element to test against 89 | domElement = document.createElement('div') 90 | ContentEdit.addCSSClass(domElement, 'foo') 91 | ContentEdit.addCSSClass(domElement, 'bar') 92 | 93 | # Remove a class that isn't defined against the DOM element (should have 94 | # no effect). 95 | ContentEdit.removeCSSClass(domElement, 'zee') 96 | expect(domElement.getAttribute('class')).toBe 'foo bar' -------------------------------------------------------------------------------- /src/spec/fixtures.coffee: -------------------------------------------------------------------------------- 1 | # Fixture 2 | 3 | describe '`ContentEdit.Fixture()`', () -> 4 | 5 | it 'should return an instance of Fixture`', () -> 6 | 7 | div = document.createElement('div') 8 | p = document.createElement('p') 9 | p.innerHTML = 'foo bar' 10 | div.appendChild(p) 11 | fixture = new ContentEdit.Fixture(p) 12 | expect(fixture instanceof ContentEdit.Fixture).toBe true 13 | 14 | # Check the child element has been correctly modified 15 | child = fixture.children[0] 16 | 17 | # The child element should state that it is fixed 18 | expect(child.isFixed()).toBe true 19 | 20 | # Th behaviour of the child element should be restricted 21 | expect(child.can('drag')).toBe false 22 | expect(child.can('drop')).toBe false 23 | expect(child.can('merge')).toBe false 24 | expect(child.can('remove')).toBe false 25 | expect(child.can('resize')).toBe false 26 | expect(child.can('spawn')).toBe false 27 | 28 | 29 | describe '`ContentEdit.Fixture.domElement()`', () -> 30 | 31 | it 'should return the DOM element of the child `Element` it wraps', () -> 32 | div = document.createElement('div') 33 | p = document.createElement('p') 34 | p.innerHTML = 'foo bar' 35 | div.appendChild(p) 36 | fixture = new ContentEdit.Fixture(p) 37 | expect(fixture.domElement()).toBe fixture.children[0].domElement() 38 | 39 | 40 | describe '`ContentEdit.Fixture.isMounted()`', () -> 41 | 42 | it 'should always return true', () -> 43 | div = document.createElement('div') 44 | p = document.createElement('p') 45 | p.innerHTML = 'foo bar' 46 | div.appendChild(p) 47 | fixture = new ContentEdit.Fixture(p) 48 | expect(fixture.isMounted()).toBe true 49 | 50 | 51 | describe '`ContentEdit.Fixture.html()`', () -> 52 | 53 | it 'should return a HTML string for the fixture', () -> 54 | # The HTML output for a fixture should typically be the same as the 55 | # inner HTML of the `Element` it wraps (though there will be exceptions, 56 | # e.g `Image`s when they are available). 57 | 58 | # Test output for `Text` element 59 | div = document.createElement('div') 60 | p = document.createElement('p') 61 | p.innerHTML = 'foo bar' 62 | div.appendChild(p) 63 | fixture = new ContentEdit.Fixture(p) 64 | expect(fixture.html()).toBe( 65 | "

\n#{ ContentEdit.INDENT }foo bar\n

" 66 | ) 67 | 68 | 69 | # Test specific to fixtures containing text elements 70 | 71 | describe '`ContentEdit.Fixture` text behaviour', () -> 72 | 73 | it 'should return trigger next/previous-region event when tab key is 74 | pressed', () -> 75 | 76 | root = ContentEdit.Root.get() 77 | 78 | div = document.createElement('div') 79 | p = document.createElement('p') 80 | p.innerHTML = 'foo bar' 81 | div.appendChild(p) 82 | fixture = new ContentEdit.Fixture(p) 83 | child = fixture.children[0] 84 | 85 | # Create event handlers 86 | handlers = { 87 | nextRegion: () -> 88 | return 89 | 90 | previousRegion: () -> 91 | return 92 | } 93 | 94 | # Spy on the event handlers 95 | spyOn(handlers, 'nextRegion') 96 | spyOn(handlers, 'previousRegion') 97 | 98 | # Bind the event handlers to the root for the events of interest 99 | root.bind('next-region', handlers.nextRegion) 100 | root.bind('previous-region', handlers.previousRegion) 101 | 102 | # Simulate tab key being pressed 103 | child._keyTab({ 104 | preventDefault: () -> 105 | }) 106 | expect(handlers.nextRegion).toHaveBeenCalledWith(fixture) 107 | 108 | # Simulate tab and shift key being pressed 109 | child._keyTab({ 110 | preventDefault: () ->, 111 | shiftKey: true 112 | }) 113 | expect(handlers.previousRegion).toHaveBeenCalledWith(fixture) -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Project configuration 4 | grunt.initConfig({ 5 | 6 | pkg: grunt.file.readJSON('package.json') 7 | 8 | coffee: 9 | options: 10 | join: true 11 | 12 | build: 13 | files: 14 | 'src/tmp/content-edit.js': [ 15 | 'src/scripts/namespace.coffee' 16 | 'src/scripts/tag-names.coffee' 17 | 'src/scripts/bases.coffee' 18 | 'src/scripts/regions.coffee' 19 | 'src/scripts/fixtures.coffee' 20 | 'src/scripts/root.coffee' 21 | 'src/scripts/static.coffee' 22 | 'src/scripts/text.coffee' 23 | 'src/scripts/images.coffee' 24 | 'src/scripts/videos.coffee' 25 | 'src/scripts/lists.coffee' 26 | 'src/scripts/tables.coffee' 27 | ] 28 | 29 | sandbox: 30 | files: 31 | 'sandbox/sandbox.js': 'src/sandbox/sandbox.coffee' 32 | 33 | spec: 34 | files: 35 | 'spec/spec-helper.js': 'src/spec/spec-helper.coffee' 36 | 'spec/content-edit-spec.js': [ 37 | 'src/spec/namespace.coffee' 38 | 'src/spec/bases.coffee' 39 | 'src/spec/tag-names.coffee' 40 | 'src/spec/bases.coffee' 41 | 'src/spec/regions.coffee' 42 | 'src/spec/fixtures.coffee' 43 | 'src/spec/root.coffee' 44 | 'src/spec/static.coffee' 45 | 'src/spec/text.coffee' 46 | 'src/spec/images.coffee' 47 | 'src/spec/videos.coffee' 48 | 'src/spec/lists.coffee' 49 | 'src/spec/tables.coffee' 50 | ] 51 | 52 | sass: 53 | options: 54 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */' 55 | sourcemap: 'none' 56 | style: 'compressed' 57 | 58 | build: 59 | files: 60 | 'build/content-edit.min.css': 'src/styles/content-edit.scss' 61 | 62 | sandbox: 63 | files: 64 | 'sandbox/sandbox.css': 'src/sandbox/sandbox.scss' 65 | 66 | uglify: 67 | options: 68 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */\n' 69 | mangle: true 70 | 71 | build: 72 | src: 'build/content-edit.js' 73 | dest: 'build/content-edit.min.js' 74 | 75 | concat: 76 | build: 77 | src: [ 78 | 'external/html-string.js' 79 | 'external/content-select.js' 80 | 'src/tmp/content-edit.js' 81 | ] 82 | dest: 'build/content-edit.js' 83 | 84 | clean: 85 | build: ['src/tmp'] 86 | 87 | jasmine: 88 | build: 89 | src: ['build/content-edit.js'] 90 | options: 91 | specs: 'spec/content-edit-spec.js' 92 | helpers: 'spec/spec-helper.js' 93 | 94 | watch: 95 | build: 96 | files: ['src/scripts/*.coffee', 'src/styles/*.scss'] 97 | tasks: ['build'] 98 | 99 | sandbox: 100 | files: [ 101 | 'src/sandbox/*.coffee', 102 | 'src/sandbox/*.scss' 103 | ] 104 | tasks: ['sandbox'] 105 | 106 | spec: 107 | files: ['src/spec/*.coffee'] 108 | tasks: ['spec'] 109 | }) 110 | 111 | # Plug-ins 112 | grunt.loadNpmTasks 'grunt-contrib-clean' 113 | grunt.loadNpmTasks 'grunt-contrib-coffee' 114 | grunt.loadNpmTasks 'grunt-contrib-concat' 115 | grunt.loadNpmTasks 'grunt-contrib-jasmine' 116 | grunt.loadNpmTasks 'grunt-contrib-sass' 117 | grunt.loadNpmTasks 'grunt-contrib-uglify' 118 | grunt.loadNpmTasks 'grunt-contrib-watch' 119 | 120 | # Tasks 121 | grunt.registerTask 'build', [ 122 | 'coffee:build' 123 | 'sass:build' 124 | 'concat:build' 125 | 'uglify:build' 126 | 'clean:build' 127 | ] 128 | 129 | grunt.registerTask 'sandbox', [ 130 | 'coffee:sandbox' 131 | 'sass:sandbox' 132 | ] 133 | 134 | grunt.registerTask 'spec', [ 135 | 'coffee:spec' 136 | ] 137 | 138 | grunt.registerTask 'watch-build', ['watch:build'] 139 | grunt.registerTask 'watch-sandbox', ['watch:sandbox'] 140 | grunt.registerTask 'watch-spec', ['watch:spec'] -------------------------------------------------------------------------------- /src/scripts/static.coffee: -------------------------------------------------------------------------------- 1 | class ContentEdit.Static extends ContentEdit.Element 2 | 3 | # A non-editable (static) HTML element. 4 | 5 | # REVIEW: The primary purpose of static elements is to provide a fallback 6 | # for when a DOM element in an editable region has not been mapped to an 7 | # editable `ContentEdit.Element` class. 8 | # 9 | # To keep the code small we don't preventively override all the various 10 | # `ContentEdit.Element` methods, but they can't safely be called and as it 11 | # stands `ContentEdit.Static` elements should not be interacted with. 12 | # 13 | # The only interaction currently supported is dropping other elements on to 14 | # a static element, without support for this interaction static elements 15 | # could make it impossible to move a static element from the start or end of 16 | # a region. 17 | # 18 | # A known problem with the content of static elements is that we rely on the 19 | # browser's interpretation of the content (because we use innerHTML), this 20 | # can lead to differences is the output as well as inconsistencies between 21 | # browsers. 22 | 23 | constructor: (tagName, attributes, content) -> 24 | super(tagName, attributes) 25 | 26 | # The associated DOM element 27 | @_content = content 28 | 29 | # Read-only properties 30 | 31 | cssTypeName: () -> 32 | return 'static' 33 | 34 | type: () -> 35 | # Return the type of element (this should be the same as the class name) 36 | return 'Static' 37 | 38 | typeName: () -> 39 | # Return the name of the element type (e.g Image, List item) 40 | return 'Static' 41 | 42 | # Methods 43 | 44 | createDraggingDOMElement: () -> 45 | # Create a DOM element that visually aids the user in dragging the 46 | # element to a new location in the editiable tree structure. 47 | unless @isMounted() 48 | return 49 | 50 | helper = super() 51 | 52 | # Use the body of the node to create the helper but limit the text to 53 | # something sensible. 54 | 55 | # HACK: This is really a best guess at displaying something appropriate 56 | # in the helper since we have no idea what's contained in a static 57 | # element. 58 | text = @_domElement.textContent 59 | if text.length > ContentEdit.HELPER_CHAR_LIMIT 60 | text = text.substr(0, ContentEdit.HELPER_CHAR_LIMIT) 61 | 62 | helper.innerHTML = text 63 | 64 | return helper 65 | 66 | html: (indent='') -> 67 | # Return a HTML string for the node 68 | 69 | # Check if element is a self closing tag 70 | if HTMLString.Tag.SELF_CLOSING[@_tagName] 71 | return "#{ indent }<#{ @_tagName }#{ @_attributesToString() }>" 72 | 73 | return "#{ indent }<#{ @_tagName }#{ @_attributesToString() }>" + 74 | "#{ @_content }" + 75 | "#{ indent }" 76 | 77 | mount: () -> 78 | # Mount the element on to the DOM 79 | 80 | # Create the DOM element to mount 81 | @_domElement = document.createElement(@_tagName) 82 | 83 | # Set the attributes 84 | for name, value of @_attributes 85 | @_domElement.setAttribute(name, value) 86 | 87 | # Set the content in the document 88 | @_domElement.innerHTML = @_content 89 | 90 | super() 91 | 92 | # NOTE: Static elements cannot receive focus. 93 | blur: undefined 94 | focus: undefined 95 | 96 | # Event handlers 97 | 98 | _onMouseDown: (ev) -> 99 | # Give the element focus 100 | super(ev) 101 | 102 | # If the static element has the moveable flag set then allow it to be 103 | # dragged to a new position. 104 | if @attr('data-ce-moveable') != undefined 105 | 106 | # We add a small delay to prevent drag engaging instantly 107 | clearTimeout(@_dragTimeout) 108 | @_dragTimeout = setTimeout( 109 | () => 110 | @drag(ev.pageX, ev.pageY) 111 | 150 112 | ) 113 | 114 | _onMouseOver: (ev) -> 115 | super(ev) 116 | 117 | # Don't highlight that we're over the element 118 | @_removeCSSClass('ce-element--over') 119 | 120 | _onMouseUp: (ev) -> 121 | super(ev) 122 | 123 | # If we're waiting to see if the user wants to drag the element, stop 124 | # waiting they don't. 125 | if @_dragTimeout 126 | clearTimeout(@_dragTimeout) 127 | 128 | # Class properties 129 | 130 | @droppers: 131 | 'Static': ContentEdit.Element._dropVert 132 | 133 | # Class methods 134 | 135 | @fromDOMElement: (domElement) -> 136 | # Convert an element (DOM) to an element of this type 137 | return new @( 138 | domElement.tagName, 139 | @getDOMElementAttributes(domElement), 140 | domElement.innerHTML 141 | ) 142 | 143 | 144 | # Register `ContentEdit.Static` the class with associated tag names 145 | ContentEdit.TagNames.get().register(ContentEdit.Static, 'static') -------------------------------------------------------------------------------- /src/spec/static.coffee: -------------------------------------------------------------------------------- 1 | # Static 2 | 3 | describe '`ContentEdit.Static()`', () -> 4 | 5 | it 'should return an instance of Static`', () -> 6 | 7 | staticElm = new ContentEdit.Static('div', {}, '
') 8 | expect(staticElm instanceof ContentEdit.Static).toBe true 9 | 10 | 11 | describe '`ContentEdit.Static.cssTypeName()`', () -> 12 | 13 | it 'should return \'static\'', () -> 14 | 15 | staticElm = new ContentEdit.Static('div', {}, '
') 16 | expect(staticElm.cssTypeName()).toBe 'static' 17 | 18 | 19 | describe '`ContentEdit.Static.createDraggingDOMElement()`', () -> 20 | 21 | it 'should create a helper DOM element', () -> 22 | # Mount an image to a region 23 | staticElm = new ContentEdit.Static('div', {}, 'foo bar') 24 | region = new ContentEdit.Region(document.createElement('div')) 25 | region.attach(staticElm) 26 | 27 | # Get the helper DOM element 28 | helper = staticElm.createDraggingDOMElement() 29 | 30 | expect(helper).not.toBe null 31 | expect(helper.tagName.toLowerCase()).toBe 'div' 32 | expect(helper.innerHTML).toBe 'foo bar' 33 | 34 | 35 | describe '`ContentEdit.Static.type()`', () -> 36 | 37 | it 'should return \'Static\'', () -> 38 | staticElm = new ContentEdit.Static('div', {}, '
') 39 | expect(staticElm.type()).toBe 'Static' 40 | 41 | 42 | describe '`ContentEdit.Static.typeName()`', () -> 43 | 44 | it 'should return \'Static\'', () -> 45 | 46 | staticElm = new ContentEdit.Static('div', {}, '
') 47 | expect(staticElm.typeName()).toBe 'Static' 48 | 49 | 50 | describe 'ContentEdit.Static.html()', () -> 51 | 52 | it 'should return a HTML string for the static element', () -> 53 | staticElm = new ContentEdit.Static( 54 | 'div', 55 | {'class': 'foo'}, 56 | '
foo
' 57 | ) 58 | expect(staticElm.html()).toBe( 59 | '
foo
' 60 | ) 61 | 62 | 63 | describe 'ContentEdit.Static.mount()', () -> 64 | 65 | region = null 66 | staticElm = null 67 | 68 | beforeEach -> 69 | staticElm = new ContentEdit.Static( 70 | 'div', 71 | {'class': 'foo'}, 72 | '
foo
' 73 | ) 74 | 75 | # Mount the static element 76 | region = new ContentEdit.Region(document.createElement('div')) 77 | region.attach(staticElm) 78 | staticElm.unmount() 79 | 80 | it 'should mount the static element to the DOM', () -> 81 | staticElm.mount() 82 | expect(staticElm.isMounted()).toBe true 83 | expect(staticElm.domElement().innerHTML).toBe '
foo
' 84 | 85 | it 'should trigger the `mount` event against the root', () -> 86 | 87 | # Create a function to call when the event is triggered 88 | foo = { 89 | handleFoo: () -> 90 | return 91 | } 92 | spyOn(foo, 'handleFoo') 93 | 94 | # Bind the function to the root for the mount event 95 | root = ContentEdit.Root.get() 96 | root.bind('mount', foo.handleFoo) 97 | 98 | # Mount the static element 99 | staticElm.mount() 100 | expect(foo.handleFoo).toHaveBeenCalledWith(staticElm) 101 | 102 | 103 | describe '`ContentEdit.Static.fromDOMElement()`', () -> 104 | 105 | it 'should convert a DOM element into an static element', () -> 106 | 107 | region = new ContentEdit.Region(document.createElement('div')) 108 | domElement = document.createElement('div') 109 | domElement.innerHTML = '
foo
' 110 | staticElm = ContentEdit.Static.fromDOMElement(domElement) 111 | region.attach(staticElm) 112 | 113 | expect(staticElm.domElement().innerHTML).toBe '
foo
' 114 | 115 | 116 | # Droppers 117 | 118 | describe '`ContentEdit.Static` drop interactions if `data-ce-moveable` is 119 | set', () -> 120 | 121 | staticElm = null 122 | region = null 123 | 124 | beforeEach -> 125 | region = new ContentEdit.Region(document.createElement('div')) 126 | staticElm = new ContentEdit.Static( 127 | 'div', 128 | {'data-ce-moveable': ''}, 129 | 'foo' 130 | ) 131 | region.attach(staticElm) 132 | 133 | it 'should support dropping on Text', () -> 134 | otherStaticElm = new ContentEdit.Static( 135 | 'div', 136 | {'data-ce-moveable': ''}, 137 | 'bar' 138 | ) 139 | region.attach(otherStaticElm) 140 | 141 | # Check the initial order 142 | expect(staticElm.nextSibling()).toBe otherStaticElm 143 | 144 | # Check the order after dropping the element after 145 | staticElm.drop(otherStaticElm, ['below', 'center']) 146 | expect(otherStaticElm.nextSibling()).toBe staticElm 147 | 148 | # Check the order after dropping the element before 149 | staticElm.drop(otherStaticElm, ['above', 'center']) 150 | expect(staticElm.nextSibling()).toBe otherStaticElm -------------------------------------------------------------------------------- /sandbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ContentEdit sandbox 6 | 7 | 8 | 9 | 10 |

11 | This is a sandbox page used to test the ContentEdit 12 | library. 13 |

14 | 15 |
20 | Some image 21 |
22 | 23 |
24 |

25 | Working towards basic
text editing support. 26 |

27 |
<head>
 28 |     <title>My page</title>
 29 |     <link rel="stylesheet" type="text/css" href="assets/content-tools.min.css">
 30 |     ...
 31 | </head>
 32 | <body>
 33 |     ...
 34 |     <script src="assets/content-tools.min.js"></script>
 35 |     <script src="assets/editor.js"></script>
 36 | </body>
37 | Example image 42 |

 Below me is a table

43 |

Above me is a table

44 | 45 |

46 | Below is an image fixture in free content 47 |

48 |
53 | Some image 54 |
55 |

56 | Above is an image fixture in free content 57 |

58 |

59 | The item below this is 60 | static. 61 |

62 |
63 |
64 | I am a static item 65 |
66 |
67 | 74 |

75 | Now starting to add support for dragging text elements around. 76 |

77 |
    78 |
  1. 79 | Item A 80 | Something bold 81 |
      82 |
    1. Sub item no. 1
    2. 83 |
    3. Sub item no. 2
    4. 84 |
    85 |
  2. 86 |
  3. 87 | Item B 88 |
      89 |
    • Sub item no. 3
    • 90 |
    • Sub item no. 4
    • 91 |
    92 |
  4. 93 |
  5. 94 | Item C 95 |
      96 |
    • Sub item no. 5
    • 97 |
    • Sub item no. 6
    • 98 |
    • Sub item no. 7
    • 99 |
    • Sub item no. 8
    • 100 |
    101 |
  6. 102 |
  7. 103 | Item <b> D 104 |
  8. 105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
PositionName
ChairHelen Troughton
Vice-chairSarah Stone
SecretaryBridget Brickley
TreasurerSarahann Holmes
Publicity officerZillah Cimmock
136 |

137 | End of one region 138 |

139 |
140 | 141 |
142 |

143 | Start of another region 144 |

145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/scripts/videos.coffee: -------------------------------------------------------------------------------- 1 | class ContentEdit.Video extends ContentEdit.ResizableElement 2 | 3 | # An editable video (e.g ). 4 | # The `Video` element supports 2 special tags to allow the the size of the 5 | # image to be constrained (data-ce-min-width, data-ce-max-width). 6 | # 7 | # NOTE: YouTube and Vimeo provide support for embedding videos using the 8 | # ' 91 | 92 | 93 | describe '`ContentEdit.Video.mount()`', () -> 94 | 95 | videoA = null 96 | videoB = null 97 | region = null 98 | 99 | beforeEach -> 100 | videoA = new ContentEdit.Video( 101 | 'video', 102 | {'controls': ''}, 103 | [ 104 | {'src': 'foo.mp4', 'type': 'video/mp4'}, 105 | {'src': 'bar.ogg', 'type': 'video/ogg'} 106 | ] 107 | ) 108 | videoB = new ContentEdit.Video( 109 | 'iframe', 110 | {'src': 'foo.mp4'} 111 | ) 112 | 113 | # Mount the videos 114 | region = new ContentEdit.Region(document.createElement('div')) 115 | region.attach(videoA) 116 | region.attach(videoB) 117 | videoA.unmount() 118 | videoB.unmount() 119 | 120 | it 'should mount the image to the DOM', () -> 121 | videoA.mount() 122 | videoB.mount() 123 | expect(videoA.isMounted()).toBe true 124 | expect(videoB.isMounted()).toBe true 125 | 126 | it 'should trigger the `mount` event against the root', () -> 127 | 128 | # Create a function to call when the event is triggered 129 | foo = { 130 | handleFoo: () -> 131 | return 132 | } 133 | spyOn(foo, 'handleFoo') 134 | 135 | # Bind the function to the root for the mount event 136 | root = ContentEdit.Root.get() 137 | root.bind('mount', foo.handleFoo) 138 | 139 | # Mount the image 140 | videoA.mount() 141 | expect(foo.handleFoo).toHaveBeenCalledWith(videoA) 142 | 143 | 144 | describe '`ContentEdit.Video.fromDOMElement()`', () -> 145 | 146 | INDENT = ContentEdit.INDENT 147 | 148 | it 'should convert a