mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 07:13:08 -05:00
Initial commit for public release.
This commit is contained in:
commit
72141fd670
10 changed files with 2716 additions and 0 deletions
74
Demo.html
Normal file
74
Demo.html
Normal file
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>HTML Editor Test</title>
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
padding: 50px;
|
||||
width: 540px;
|
||||
font: 400 14px/1.24 helvetica, arial, sans-serif;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.95em;
|
||||
}
|
||||
iframe {
|
||||
border: 1px solid #888;
|
||||
}
|
||||
span {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var editor;
|
||||
document.addEventListener( 'click', function ( e ) {
|
||||
var id = e.target.id,
|
||||
value;
|
||||
if ( id && editor && editor[ id ] ) {
|
||||
if ( e.target.className === 'prompt' ) {
|
||||
value = prompt( 'Value:' );
|
||||
}
|
||||
editor[ id ]( value );
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTML Editor Test</h1>
|
||||
<header>
|
||||
<p>This is a really simple demo, with the most trivial of UI integrations</p>
|
||||
<p>
|
||||
<span id="bold">Bold</span>
|
||||
<span id="removeBold">Unbold</span>
|
||||
<span id="italic">Italic</span>
|
||||
|
||||
<span id="removeItalic">Unitalic</span>
|
||||
<span id="underline">Underline</span>
|
||||
<span id="removeUnderline">Deunderline</span>
|
||||
|
||||
<span id="setFontSize" class="prompt">Font size</span>
|
||||
<span id="setFontFace" class="prompt">Font face</span>
|
||||
</p>
|
||||
<p>
|
||||
<span id="incQuoteLevel">Quote</span>
|
||||
<span id="decQuoteLevel">Dequote</span>
|
||||
|
||||
<span id="makeUnorderedList">List</span>
|
||||
<span id="removeList">Unlist</span>
|
||||
|
||||
<span id="insertImage" class="prompt">Insert image</span>
|
||||
|
||||
<span id="undo">Undo</span>
|
||||
<span id="redo">Redo</span>
|
||||
</p>
|
||||
</header>
|
||||
<iframe src="build/document.html" onload="top.editor=this.contentWindow.editor" width="500" height="500"></iframe>
|
||||
</body>
|
||||
</html>
|
19
License.txt
Normal file
19
License.txt
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright © 2011 by Neil Jenkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
14
Makefile
Normal file
14
Makefile
Normal file
|
@ -0,0 +1,14 @@
|
|||
.PHONY: build clean
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
build: build/squire.js build/document.html
|
||||
|
||||
build/squire.js: source/Node.js source/Range.js source/Editor.js
|
||||
mkdir -p $(@D)
|
||||
cat $^ | uglifyjs > $@
|
||||
|
||||
build/document.html: source/document.html
|
||||
mkdir -p $(@D)
|
||||
cp $^ $@
|
306
Readme.md
Normal file
306
Readme.md
Normal file
|
@ -0,0 +1,306 @@
|
|||
Squire
|
||||
======
|
||||
|
||||
Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation, whilst being supremely lightweight and flexible. It is built for the present and the future, and as such does not support ancient browsers. I'd guess it should work fine back to around Opera 10, Firefox 3.5, Safari 4, Chrome 9 and IE9, but I only test in the latest version of each of these browsers. Adding IE8 support should be possible by patching IE8 to support the W3C Range and TreeWalker APIs; patches are welcome. I am not interested in support for IE7 or below.
|
||||
|
||||
Unlike other HTML5 rich text editors, squire was written as a component for writing documents (emails, essays, etc.), not doing wysiwyg websites. If you are looking for support for inserting form controls or flash components or the like, you'll need to look elsewhere. However for many purposes, Squire may be just what you need, providing the power without the bloat. The key features are:
|
||||
|
||||
### Lightweight ###
|
||||
|
||||
* Only 7KB of JS after minification and gzip (25KB before gzip).
|
||||
* Does not include its own XHR wrapper, widget library or lightbox overlays.
|
||||
* No dependencies.
|
||||
* No UI for a toolbar is supplied, allowing you to integrate seamlessly with the
|
||||
rest of your application and lose the bloat of having two UI toolkits loaded.
|
||||
Instead, you get a component you can drop in in place of a `<textarea>` and
|
||||
manipulate programatically.
|
||||
|
||||
### Powerful ###
|
||||
|
||||
Squire provides an engine that handles the heavy work for you, making it easy to
|
||||
add extra features. With the `changeFormat` method you can easily add or remove any inline formatting you wish. And the `modifyBlocks` method can be used to make complicated block-level changes in a relatively easy manner.
|
||||
|
||||
If you need more commands than in the simple API, I suggest you check out the source code (it's not very long), and see how a lot of the other API methods are implemented in terms of these two methods.
|
||||
|
||||
The general philosophy of Squire is to allow the browser to do as much as it can (which unfortunately is not very much), but take control anywhere it deviates from what is required, or there are significant cross-browser differences. As such, the `document.execCommand` method is not used at all; instead all formatting is done via custom functions, and certain keys, such as 'enter' and 'backspace' are handled by the editor.
|
||||
|
||||
Installation and usage
|
||||
----------------------
|
||||
|
||||
1. Copy the contents of the `build/` directory onto your server.
|
||||
2. Edit the `<style>` block in document.html to add the default styles you would
|
||||
like the editor to use (or link to an external stylesheet).
|
||||
3. In your application, instead of a `<textarea>`, use an
|
||||
`<iframe src="path/to/document.html">`.
|
||||
4. In your JS, attach an event listener to the `load` event of the iframe. When
|
||||
this fires you can grab a reference to the editor object through
|
||||
`iframe.contentWindow.editor`.
|
||||
5. Use the API below with the `editor` object to set and get data and integrate
|
||||
with your application or framework.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Squire is released under the MIT license. See License.txt for full license.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
### addEventListener ###
|
||||
|
||||
Attach an event listener to the editor. The handler can be either a function or an object with a `handleEvent` method. This function or method will be called whenever the event fires, with an event object as the sole argument. The following events may be observed:
|
||||
|
||||
* **focus**: The editor gained focus.
|
||||
* **blur**: The editor lost focus
|
||||
* **keydown**: Standard DOM keydown event.
|
||||
* **keypress**: Standard DOM keypress event.
|
||||
* **keyup**: Standard DOM keyup event.
|
||||
* **input**: The user inserted, deleted or changed the style of some text; in other words, the result for `editor.getHTML()` will have changed.
|
||||
* **pathChange**: The path (see getPath documentation) to the cursor has changed. The new path is available as the `data` property on the event object.
|
||||
* **select**: The user selected some text
|
||||
* **undoStateChange**: The availability of undo and/or redo has changed. The event object has two boolean properties, `canUndo` and `canRedo` to let you know the new state.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **type**: The event to listen for. e.g. 'focus'.
|
||||
* **handler**: The callback function to invoke
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### removeEventListener ###
|
||||
|
||||
Remove an event listener attached via the addEventListener method.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **type**: The event type the handler was registered for.
|
||||
* **handler**: The handler to remove.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### focus ###
|
||||
|
||||
Focuses the editor.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### blur ###
|
||||
|
||||
Removes focus from the editor
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### getDocument ###
|
||||
|
||||
Returns the `document` object of the editable area. May be useful to do transformations outside the realm of the API.
|
||||
|
||||
### getHTML ###
|
||||
|
||||
Returns the HTML value of the editor in its current state. This value is equivalent to the contents of the `<body>` tag and does not include any surrounding boilerplate.
|
||||
|
||||
### setHTML ###
|
||||
|
||||
Sets the HTML value for the editor. The value supplied should not contain `<body>` tags or anything outside of that.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **html**: The html to set.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
#### insertImage ####
|
||||
|
||||
Inserts an image at the current cursor location.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **src**: The source path for the image.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns a reference to the newly inserted image element.
|
||||
|
||||
### getPath ###
|
||||
|
||||
Returns the path through the DOM tree from the `<body>` element to the current current cursor position. This is a string consisting of the tag, id and class names in CSS format. For example `BODY>BLOCKQUOTE>DIV#id>STRONG>SPAN.font>EM`. If a selection has been made, so different parts of the selection may have different paths, the value will be `(selection)`. The path is useful for efficiently determining the current formatting for bold, italic, underline etc, and thus determining button state. If a selection has been made, you can has the `hasFormat` method instead to get the current state for the properties you care about.
|
||||
|
||||
### getSelection ###
|
||||
|
||||
Returns a W3C Range object representing the current selection/cursor position.
|
||||
|
||||
### setSelection ###
|
||||
|
||||
Changes the current selection/cursor position.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **range**: The W3C Range object representing the desired selection.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### undo ###
|
||||
|
||||
Undoes the most recent change.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### redo ###
|
||||
|
||||
If the user has just undone a change, this will reapply that change.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### hasFormat ###
|
||||
|
||||
Queries the editor for whether a particular format is applied anywhere in the current selection.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **tag**: The tag of the format
|
||||
* **attributes**: (optional) Any attributes the format.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns `true` if any of the selection is contained within an element with the specified tag and attributes, otherwise returns `false`.
|
||||
|
||||
### bold ###
|
||||
|
||||
Makes any non-bold currently selected text bold (by wrapping it in a `<strong>` tag).
|
||||
|
||||
Returns self.
|
||||
|
||||
### italic ###
|
||||
|
||||
Makes any non-italic currently selected text italic (by wrapping it in an `<em>` tag).
|
||||
|
||||
Returns self.
|
||||
|
||||
### underline ###
|
||||
|
||||
Makes any non-underlined currently selected text underlined (by wrapping it in a `<u>` tag).
|
||||
|
||||
Returns self.
|
||||
|
||||
### removeBold ###
|
||||
|
||||
Removes any bold formatting from the selected text.
|
||||
|
||||
Returns self.
|
||||
|
||||
### removeItalic ###
|
||||
|
||||
Removes any italic formatting from the selected text.
|
||||
|
||||
Returns self.
|
||||
|
||||
### removeUnderline ###
|
||||
|
||||
Removes any underline formatting from the selected text.
|
||||
|
||||
Returns self.
|
||||
|
||||
### makeLink ###
|
||||
|
||||
Makes the currently selected text a link.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **url**: The url to link to.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### setFontFace ###
|
||||
|
||||
Sets the font face for the selected text.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **font**: A comma-separated list of fonts (in order of preference) to set.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### setFontSize ###
|
||||
|
||||
Sets the font size for the selected text.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **size**: A size to set. Any CSS size value is accepted, e.g. '13px', or 'small'.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### setTextAlignment ###
|
||||
|
||||
Sets the text alignment in all blocks at least partially contained by the selection.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **alignment**: The direction to align to. Can be 'left', 'right', 'center' or 'justify'.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### modifyBlocks ###
|
||||
|
||||
Extracts a portion of the DOM tree (up to the block boundaries of the current selection), modifies it and then reinserts it and merges the edges. See the code for examples if you're interested in using this function.
|
||||
|
||||
#### Parameters ####
|
||||
|
||||
* **modify** The function to apply to the extracted DOM tree; gets a document fragment as a sole argument. Should return the node or fragment to be reinserted in the DOM.
|
||||
|
||||
#### Returns ####
|
||||
|
||||
Returns self.
|
||||
|
||||
### incQuoteLevel ###
|
||||
|
||||
Increases by 1 the quote level (number of `<blockquote>` tags wrapping) all blocks at least partially selected.
|
||||
|
||||
Returns self.
|
||||
|
||||
### decQuoteLevel ###
|
||||
|
||||
Decreases by 1 the quote level (number of `<blockquote>` tags wrapping) all blocks at least partially selected.
|
||||
|
||||
Returns self.
|
||||
|
||||
### makeUnorderedList ###
|
||||
|
||||
Changes all at-least-partially selected blocks to be part of an unordered list.
|
||||
|
||||
Returns self.
|
||||
|
||||
### makeOrderedList ###
|
||||
|
||||
Changes all at-least-partially selected blocks to be part of an ordered list.
|
||||
|
||||
Returns self.
|
||||
|
||||
### removeList ###
|
||||
|
||||
Changes any at-least-partially selected blocks which are part of a list to no longer be part of a list.
|
||||
|
||||
Returns self.
|
57
build/document.html
Normal file
57
build/document.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
background: transparent;
|
||||
color: #2b2b2b;
|
||||
font: 13px/1.35 Helvetica, arial, sans-serif;
|
||||
cursor: text;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
h1 {
|
||||
font-size: 138.5%;
|
||||
}
|
||||
h2 {
|
||||
font-size: 123.1%;
|
||||
}
|
||||
h3 {
|
||||
font-size: 108%;
|
||||
}
|
||||
h1,h2,h3,p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
h4,h5,h6 {
|
||||
margin: 0;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid blue;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
<!--[if IE 9]>
|
||||
<script type="text/javascript">window.ie = 9;</script>
|
||||
<![endif]-->
|
||||
<script type="text/javascript" src="squire.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
1
build/squire.js
Normal file
1
build/squire.js
Normal file
File diff suppressed because one or more lines are too long
1326
source/Editor.js
Normal file
1326
source/Editor.js
Normal file
File diff suppressed because it is too large
Load diff
452
source/Node.js
Normal file
452
source/Node.js
Normal file
|
@ -0,0 +1,452 @@
|
|||
/* Copyright © 2011 by Neil Jenkins. Licensed under the MIT license. */
|
||||
|
||||
( function () {
|
||||
|
||||
/*global Node, Text, Element, window, document */
|
||||
|
||||
"use strict";
|
||||
|
||||
var implement = function ( constructor, props ) {
|
||||
var proto = constructor.prototype,
|
||||
prop;
|
||||
for ( prop in props ) {
|
||||
proto[ prop ] = props[ prop ];
|
||||
}
|
||||
};
|
||||
|
||||
var every = function ( nodeList, fn ) {
|
||||
var l = nodeList.length;
|
||||
while ( l-- ) {
|
||||
if ( !fn( nodeList[l] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var $False = function () { return false; };
|
||||
var $True = function () { return true; };
|
||||
|
||||
var inlineNodeNames = /^(?:A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/;
|
||||
|
||||
var swap = function( node, node2 ) {
|
||||
var parent = node2.parentNode;
|
||||
if ( parent ) {
|
||||
parent.replaceChild( node, node2 );
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
var ELEMENT_NODE = 1, // Node.ELEMENT_NODE,
|
||||
TEXT_NODE = 3, // Node.TEXT_NODE,
|
||||
SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT,
|
||||
FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT,
|
||||
FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP;
|
||||
|
||||
var walkForward = function ( current, filter ) {
|
||||
var node;
|
||||
while ( true ) {
|
||||
node = current.firstChild;
|
||||
while ( !node && current ) {
|
||||
node = current.nextSibling;
|
||||
if ( !node ) {
|
||||
current = current.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
if ( node.nodeName === 'BODY' ) {
|
||||
return null;
|
||||
}
|
||||
if ( filter( node ) ) {
|
||||
return node;
|
||||
}
|
||||
|
||||
current = node;
|
||||
}
|
||||
};
|
||||
|
||||
var walkBackward = function ( current, filter ) {
|
||||
var node;
|
||||
while ( true ) {
|
||||
node = current.previousSibling;
|
||||
if ( node ) {
|
||||
while ( current = node.lastChild ) {
|
||||
node = current;
|
||||
}
|
||||
} else {
|
||||
node = current.parentNode;
|
||||
}
|
||||
|
||||
if ( node.nodeName === 'BODY' ) {
|
||||
return null;
|
||||
}
|
||||
if ( filter( node ) ) {
|
||||
return node;
|
||||
}
|
||||
|
||||
current = node;
|
||||
}
|
||||
};
|
||||
|
||||
var isBlock = function ( el ) { return el.isBlock(); };
|
||||
var useTextFixer = !!( window.opera || window.ie );
|
||||
|
||||
implement( Node, {
|
||||
isInline: $False,
|
||||
isBlock: $False,
|
||||
isContainer: $False,
|
||||
getPath: function () {
|
||||
var parent = this.parentNode;
|
||||
return parent ? parent.getPath() : '';
|
||||
},
|
||||
detach: function () {
|
||||
var parent = this.parentNode;
|
||||
if ( parent ) {
|
||||
parent.removeChild( this );
|
||||
}
|
||||
return this;
|
||||
},
|
||||
replaceWith: function ( node ) {
|
||||
swap( node, this );
|
||||
return this;
|
||||
},
|
||||
replaces: function ( node ) {
|
||||
swap( this, node );
|
||||
return this;
|
||||
},
|
||||
nearest: function ( tag, attributes ) {
|
||||
var parent = this.parentNode;
|
||||
return parent ? parent.nearest( tag, attributes ) : null;
|
||||
},
|
||||
getPreviousBlock: function () {
|
||||
return walkBackward( this, isBlock );
|
||||
},
|
||||
getNextBlock: function () {
|
||||
return walkForward( this, isBlock );
|
||||
},
|
||||
split: function ( node, stopCondition ) {
|
||||
return node;
|
||||
},
|
||||
mergeContainers: function () {}
|
||||
});
|
||||
|
||||
implement( Text, {
|
||||
isInline: $True,
|
||||
getLength: function () {
|
||||
return this.length;
|
||||
},
|
||||
isLike: function ( node ) {
|
||||
return node.nodeType === TEXT_NODE;
|
||||
},
|
||||
split: function ( offset, stopCondition ) {
|
||||
var node = this;
|
||||
if ( stopCondition( node ) ) {
|
||||
return offset;
|
||||
}
|
||||
return node.parentNode.split( node.splitText( offset ), stopCondition );
|
||||
}
|
||||
});
|
||||
|
||||
implement( Element, {
|
||||
isInline: function () {
|
||||
return inlineNodeNames.test( this.nodeName );
|
||||
},
|
||||
isBlock: function () {
|
||||
return !this.isInline() && every( this.childNodes, function ( child ) {
|
||||
return child.isInline();
|
||||
});
|
||||
},
|
||||
isContainer: function () {
|
||||
return !this.isInline() && !this.isBlock();
|
||||
},
|
||||
getLength: function () {
|
||||
return this.childNodes.length;
|
||||
},
|
||||
getPath: function() {
|
||||
var tag = this.nodeName;
|
||||
if ( tag === 'BODY' ) {
|
||||
return tag;
|
||||
}
|
||||
var path = this.parentNode.getPath(),
|
||||
id = this.id,
|
||||
className = this.className.trim();
|
||||
|
||||
path += '>' + tag;
|
||||
if ( id ) {
|
||||
path += '#' + id;
|
||||
}
|
||||
if ( className ) {
|
||||
className = className.split( /\s\s*/ );
|
||||
className.sort();
|
||||
path += '.';
|
||||
path += className.join( '.' );
|
||||
}
|
||||
return path;
|
||||
},
|
||||
wraps: function ( node ) {
|
||||
swap( this, node ).appendChild( node );
|
||||
return this;
|
||||
},
|
||||
empty: function () {
|
||||
var frag = this.ownerDocument.createDocumentFragment(),
|
||||
l = this.childNodes.length;
|
||||
while ( l-- ) {
|
||||
frag.appendChild( this.firstChild );
|
||||
}
|
||||
return frag;
|
||||
},
|
||||
is: function ( tag, attributes ) {
|
||||
if ( this.nodeName !== tag ) { return false; }
|
||||
var attr;
|
||||
for ( attr in attributes ) {
|
||||
if ( this.getAttribute( attr ) !== attributes[ attr ] ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
nearest: function ( tag, attributes ) {
|
||||
var el = this;
|
||||
do {
|
||||
if ( el.is( tag, attributes ) ) {
|
||||
return el;
|
||||
}
|
||||
} while ( ( el = el.parentNode ) &&
|
||||
( el.nodeType === ELEMENT_NODE ) );
|
||||
return null;
|
||||
},
|
||||
isLike: function ( node ) {
|
||||
return (
|
||||
node.nodeType === ELEMENT_NODE &&
|
||||
node.nodeName === this.nodeName &&
|
||||
node.className === this.className &&
|
||||
node.style.cssText === this.style.cssText
|
||||
);
|
||||
},
|
||||
mergeInlines: function ( range ) {
|
||||
var children = this.childNodes,
|
||||
l = children.length,
|
||||
frags = [],
|
||||
child, prev, len;
|
||||
while ( l-- ) {
|
||||
child = children[l];
|
||||
prev = l && children[ l - 1 ];
|
||||
if ( l && child.isInline() && child.isLike( prev ) ) {
|
||||
if ( range.startContainer === child ) {
|
||||
range.startContainer = prev;
|
||||
range.startOffset += prev.getLength();
|
||||
}
|
||||
if ( range.endContainer === child ) {
|
||||
range.endContainer = prev;
|
||||
range.endOffset += prev.getLength();
|
||||
}
|
||||
if ( range.startContainer === this ) {
|
||||
if ( range.startOffset > l ) {
|
||||
range.startOffset -= 1;
|
||||
}
|
||||
else if ( range.startOffset === l ) {
|
||||
range.startContainer = prev;
|
||||
range.startOffset = prev.getLength();
|
||||
}
|
||||
}
|
||||
if ( range.endContainer === this ) {
|
||||
if ( range.endOffset > l ) {
|
||||
range.endOffset -= 1;
|
||||
}
|
||||
else if ( range.endOffset === l ) {
|
||||
range.endContainer = prev;
|
||||
range.endOffset = prev.getLength();
|
||||
}
|
||||
}
|
||||
child.detach();
|
||||
if ( child.nodeType === TEXT_NODE ) {
|
||||
prev.appendData( child.data );
|
||||
}
|
||||
else {
|
||||
frags.push( child.empty() );
|
||||
}
|
||||
}
|
||||
else if ( child.nodeType === ELEMENT_NODE ) {
|
||||
len = frags.length;
|
||||
while ( len-- ) {
|
||||
child.appendChild( frags.pop() );
|
||||
}
|
||||
child.mergeInlines( range );
|
||||
}
|
||||
}
|
||||
},
|
||||
mergeWithBlock: function ( next, range ) {
|
||||
var block = this,
|
||||
container = next,
|
||||
last, offset, _range;
|
||||
while ( container.parentNode.childNodes.length === 1 ) {
|
||||
container = container.parentNode;
|
||||
}
|
||||
container.detach();
|
||||
|
||||
offset = block.childNodes.length;
|
||||
|
||||
// Remove extra <BR> fixer if present.
|
||||
last = block.lastChild;
|
||||
if ( last && last.nodeName === 'BR' ) {
|
||||
block.removeChild( last );
|
||||
offset -= 1;
|
||||
}
|
||||
|
||||
_range = {
|
||||
startContainer: block,
|
||||
startOffset: offset,
|
||||
endContainer: block,
|
||||
endOffset: offset
|
||||
};
|
||||
|
||||
block.appendChild( next.empty() );
|
||||
block.mergeInlines( _range );
|
||||
|
||||
range.setStart(
|
||||
_range.startContainer, _range.startOffset );
|
||||
range.collapse( true );
|
||||
|
||||
// Opera inserts a BR if you delete the last piece of text
|
||||
// in a block-level element. Unfortunately, it then gets
|
||||
// confused when setting the selection subsequently and
|
||||
// refuses to accept the range that finishes just before the
|
||||
// BR. Removing the BR fixes the bug.
|
||||
// Steps to reproduce bug: Type "a-b-c" (where - is return)
|
||||
// then backspace twice. The cursor goes to the top instead
|
||||
// of after "b".
|
||||
if ( window.opera && ( last = block.lastChild ) &&
|
||||
last.nodeName === 'BR' ) {
|
||||
block.removeChild( last );
|
||||
}
|
||||
},
|
||||
mergeContainers: function () {
|
||||
var prev = this.previousSibling,
|
||||
first = this.firstChild;
|
||||
if ( prev && prev.isLike( this ) && prev.isContainer() ) {
|
||||
prev.appendChild( this.detach().empty() );
|
||||
if ( first ) {
|
||||
first.mergeContainers();
|
||||
}
|
||||
}
|
||||
},
|
||||
split: function ( childNodeToSplitBefore, stopCondition ) {
|
||||
var node = this;
|
||||
|
||||
if ( typeof( childNodeToSplitBefore ) === 'number' ) {
|
||||
childNodeToSplitBefore = node.childNodes[ childNodeToSplitBefore ];
|
||||
}
|
||||
|
||||
if ( stopCondition( node ) ) {
|
||||
return childNodeToSplitBefore;
|
||||
}
|
||||
|
||||
// Clone node without children
|
||||
var parent = node.parentNode,
|
||||
clone = node.cloneNode( false ),
|
||||
next;
|
||||
|
||||
// Add right-hand siblings to the clone
|
||||
while ( childNodeToSplitBefore ) {
|
||||
next = childNodeToSplitBefore.nextSibling;
|
||||
clone.appendChild( childNodeToSplitBefore );
|
||||
childNodeToSplitBefore = next;
|
||||
}
|
||||
|
||||
// DO NOT NORMALISE. This may undo the fixCursor() call
|
||||
// of a node lower down the tree!
|
||||
|
||||
// We need something in the element in order for the cursor to appear.
|
||||
node.fixCursor();
|
||||
clone.fixCursor();
|
||||
|
||||
// Inject clone after original node
|
||||
if ( next = node.nextSibling ) {
|
||||
parent.insertBefore( clone, next );
|
||||
} else {
|
||||
parent.appendChild( clone );
|
||||
}
|
||||
|
||||
// Keep on splitting up the tree
|
||||
return parent.split( clone, stopCondition );
|
||||
},
|
||||
fixCursor: function () {
|
||||
// In Webkit and Gecko, block level elements are collapsed and
|
||||
// unfocussable if they have no content. To remedy this, a <BR> must be
|
||||
// inserted. In Opera and IE, we just need a textnode in order for the
|
||||
// cursor to appear.
|
||||
var el = this,
|
||||
doc = el.ownerDocument,
|
||||
fixer, child;
|
||||
|
||||
if ( el.nodeName === 'BODY' ) {
|
||||
if ( !el.firstChild ) {
|
||||
fixer = doc.createElement( 'DIV' );
|
||||
el.appendChild( fixer );
|
||||
el = fixer;
|
||||
fixer = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( el.isInline() ) {
|
||||
if ( !el.firstChild ) {
|
||||
fixer = doc.createTextNode( /* isWebkit ? '\u200B' :*/ '' );
|
||||
}
|
||||
} else {
|
||||
if ( useTextFixer ) {
|
||||
while ( el.nodeType !== TEXT_NODE ) {
|
||||
child = el.firstChild;
|
||||
if ( !child ) {
|
||||
fixer = doc.createTextNode( '' );
|
||||
break;
|
||||
}
|
||||
el = child;
|
||||
}
|
||||
}
|
||||
else if ( !el.textContent && !el.querySelector( 'BR' ) ) {
|
||||
fixer = doc.createElement( 'BR' );
|
||||
while ( ( child = el.lastElementChild ) && !child.isInline() ) {
|
||||
el = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( fixer ) {
|
||||
el.appendChild( fixer );
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// Fix IE9's buggy implementation of Text#splitText.
|
||||
// If the split is at the end of the node, it doesn't insert the newly split
|
||||
// node into the document, and sets its value to undefined rather than ''.
|
||||
// And even if the split is not at the end, the original node is removed from
|
||||
// the document and replaced by another, rather than just having its data
|
||||
// shortened.
|
||||
if ( function () {
|
||||
var div = document.createElement( 'div' ),
|
||||
text = document.createTextNode( '12' );
|
||||
div.appendChild( text );
|
||||
text.splitText( 2 );
|
||||
return div.childNodes.length !== 2;
|
||||
}() ) {
|
||||
Text.prototype.splitText = function ( offset ) {
|
||||
var afterSplit = this.ownerDocument.createTextNode(
|
||||
this.data.slice( offset ) ),
|
||||
next = this.nextSibling,
|
||||
parent = this.parentNode,
|
||||
toDelete = this.length - offset;
|
||||
if ( next ) {
|
||||
parent.insertBefore( afterSplit, next );
|
||||
} else {
|
||||
parent.appendChild( afterSplit );
|
||||
}
|
||||
if ( toDelete ) {
|
||||
this.deleteData( offset, toDelete );
|
||||
}
|
||||
return afterSplit;
|
||||
};
|
||||
}
|
||||
|
||||
}() );
|
410
source/Range.js
Normal file
410
source/Range.js
Normal file
|
@ -0,0 +1,410 @@
|
|||
/* Copyright © 2011 by Neil Jenkins. Licensed under the MIT license. */
|
||||
|
||||
( function () {
|
||||
|
||||
/*global Range, Node */
|
||||
|
||||
"use strict";
|
||||
|
||||
var implement = function ( constructor, props ) {
|
||||
var proto = constructor.prototype,
|
||||
prop;
|
||||
for ( prop in props ) {
|
||||
proto[ prop ] = props[ prop ];
|
||||
}
|
||||
};
|
||||
|
||||
var indexOf = Array.prototype.indexOf;
|
||||
|
||||
var ELEMENT_NODE = 1, // Node.ELEMENT_NODE
|
||||
TEXT_NODE = 3, // Node.TEXT_NODE
|
||||
START_TO_START = 0, // Range.START_TO_START
|
||||
START_TO_END = 1, // Range.START_TO_END
|
||||
END_TO_END = 2, // Range.END_TO_END
|
||||
END_TO_START = 3; // Range.END_TO_START
|
||||
|
||||
implement( Range, {
|
||||
_insertNode: function ( node ) {
|
||||
// Insert at start.
|
||||
var startContainer = this.startContainer,
|
||||
startOffset = this.startOffset,
|
||||
endContainer = this.endContainer,
|
||||
endOffset = this.endOffset,
|
||||
parent, children, childCount, afterSplit;
|
||||
|
||||
if ( startContainer.nodeType === TEXT_NODE ) {
|
||||
parent = startContainer.parentNode;
|
||||
children = parent.childNodes;
|
||||
if ( startOffset ) {
|
||||
afterSplit = startContainer.splitText( startOffset );
|
||||
if ( endContainer === startContainer ) {
|
||||
endOffset -= startOffset;
|
||||
endContainer = afterSplit;
|
||||
}
|
||||
else if ( endContainer === parent ) {
|
||||
endOffset += 1;
|
||||
}
|
||||
startContainer = afterSplit;
|
||||
}
|
||||
startOffset = indexOf.call( children, startContainer );
|
||||
startContainer = parent;
|
||||
} else {
|
||||
children = startContainer.childNodes;
|
||||
}
|
||||
|
||||
childCount = children.length;
|
||||
|
||||
if ( startOffset === childCount) {
|
||||
startContainer.appendChild( node );
|
||||
} else {
|
||||
startContainer.insertBefore( node, children[ startOffset ] );
|
||||
}
|
||||
if ( startContainer === endContainer ) {
|
||||
endOffset += children.length - childCount;
|
||||
}
|
||||
|
||||
this.setStart( startContainer, startOffset );
|
||||
this.setEnd( endContainer, endOffset );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_extractContents: function ( common ) {
|
||||
var startContainer = this.startContainer,
|
||||
startOffset = this.startOffset,
|
||||
endContainer = this.endContainer,
|
||||
endOffset = this.endOffset;
|
||||
|
||||
if ( !common ) {
|
||||
common = this.commonAncestorContainer;
|
||||
}
|
||||
|
||||
if ( common.nodeType === TEXT_NODE ) {
|
||||
common = common.parentNode;
|
||||
}
|
||||
|
||||
var uptoCommon = function ( node ) {
|
||||
return node === common;
|
||||
},
|
||||
endNode = endContainer.split( endOffset, uptoCommon ) || null,
|
||||
startNode = startContainer.split( startOffset, uptoCommon ),
|
||||
frag = common.ownerDocument.createDocumentFragment(),
|
||||
next;
|
||||
|
||||
// End node will be null if at end of child nodes list.
|
||||
while ( startNode !== endNode ) {
|
||||
next = startNode.nextSibling;
|
||||
frag.appendChild( startNode );
|
||||
startNode = next;
|
||||
}
|
||||
|
||||
this.setStart( common, endNode ?
|
||||
indexOf.call( common.childNodes, endNode ) :
|
||||
common.childNodes.length );
|
||||
this.collapse( true );
|
||||
|
||||
common.fixCursor();
|
||||
|
||||
return frag;
|
||||
},
|
||||
|
||||
_deleteContents: function () {
|
||||
// Move boundaries up as much as possible to reduce need to split.
|
||||
this.moveBoundariesUpTree();
|
||||
|
||||
// Remove selected range
|
||||
this._extractContents();
|
||||
|
||||
// If we split into two different blocks, merge the blocks.
|
||||
this.moveBoundariesDownTree();
|
||||
|
||||
var startBlock = this.getStartBlock(),
|
||||
endBlock = this.getEndBlock();
|
||||
if ( startBlock !== endBlock ) {
|
||||
startBlock.mergeWithBlock( endBlock, this );
|
||||
}
|
||||
|
||||
// Ensure body has a block-level element in it.
|
||||
var doc = startBlock.ownerDocument,
|
||||
body = doc.body,
|
||||
bodyFirstChild = body.firstChild;
|
||||
if ( !bodyFirstChild || bodyFirstChild.nodeName === 'BR' ) {
|
||||
startBlock = doc.createElement( 'DIV' ).fixCursor();
|
||||
if ( bodyFirstChild ) {
|
||||
body.replaceChild( startBlock, bodyFirstChild );
|
||||
}
|
||||
else {
|
||||
body.appendChild( startBlock );
|
||||
}
|
||||
this.selectNodeContents( startBlock );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
insertTreeFragment: function ( frag ) {
|
||||
// Check if it's all inline content
|
||||
var isInline = true,
|
||||
children = frag.childNodes,
|
||||
l = children.length;
|
||||
while ( l-- ) {
|
||||
if ( !children[l].isInline() ) {
|
||||
isInline = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any selected content
|
||||
if ( !this.collapsed ) {
|
||||
this._deleteContents();
|
||||
}
|
||||
|
||||
// Move range down into text ndoes
|
||||
this.moveBoundariesDownTree();
|
||||
|
||||
// If inline, just insert at the current position.
|
||||
if ( isInline ) {
|
||||
this._insertNode( frag );
|
||||
this.collapse( false );
|
||||
}
|
||||
// Otherwise, split up to body, insert inline before and after split
|
||||
// and insert block in between split, then merge containers.
|
||||
else {
|
||||
var nodeAfterSplit = this.startContainer.split( this.startOffset,
|
||||
function ( node ) { return node.nodeName === 'BODY'; }),
|
||||
nodeBeforeSplit = nodeAfterSplit.previousSibling,
|
||||
startContainer = nodeBeforeSplit,
|
||||
startOffset = startContainer.childNodes.length,
|
||||
endContainer = nodeAfterSplit,
|
||||
endOffset = 0,
|
||||
parent = nodeAfterSplit.parentNode,
|
||||
child;
|
||||
|
||||
while ( ( child = startContainer.lastChild ) &&
|
||||
child.nodeType === ELEMENT_NODE &&
|
||||
child.nodeName !== 'BR' ) {
|
||||
startContainer = child;
|
||||
startOffset = startContainer.childNodes.length;
|
||||
}
|
||||
while ( ( child = endContainer.firstChild ) &&
|
||||
child.nodeType === ELEMENT_NODE &&
|
||||
child.nodeName !== 'BR' ) {
|
||||
endContainer = child;
|
||||
}
|
||||
while ( ( child = frag.firstChild ) && child.isInline() ) {
|
||||
startContainer.appendChild( child );
|
||||
}
|
||||
while ( ( child = frag.lastChild ) && child.isInline() ) {
|
||||
endContainer.insertBefore( child, endContainer.firstChild );
|
||||
endOffset += 1;
|
||||
}
|
||||
parent.insertBefore( frag, nodeAfterSplit );
|
||||
|
||||
// 6. Merge containers at edges
|
||||
nodeAfterSplit.mergeContainers();
|
||||
nodeBeforeSplit.nextSibling.mergeContainers();
|
||||
|
||||
// Merge containers
|
||||
this.setStart( startContainer, startOffset );
|
||||
this.setEnd( endContainer, endOffset );
|
||||
}
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
containsNode: function ( node, partial ) {
|
||||
var range = this,
|
||||
nodeRange = node.ownerDocument.createRange();
|
||||
|
||||
nodeRange.selectNodeContents( node );
|
||||
|
||||
if ( partial ) {
|
||||
// Node must not finish before range starts or start after range
|
||||
// finishes.
|
||||
var nodeEndBeforeStart = ( range.compareBoundaryPoints(
|
||||
END_TO_START, nodeRange ) === 1 ),
|
||||
nodeStartAfterEnd = ( range.compareBoundaryPoints(
|
||||
START_TO_END, nodeRange ) === -1 );
|
||||
return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
|
||||
}
|
||||
else {
|
||||
// Node must start after range starts and finish before range
|
||||
// finishes
|
||||
var nodeStartAfterStart = ( range.compareBoundaryPoints(
|
||||
START_TO_START, nodeRange ) === -1 ),
|
||||
nodeEndBeforeEnd = ( range.compareBoundaryPoints(
|
||||
END_TO_END, nodeRange ) === 1 );
|
||||
return ( nodeStartAfterStart && nodeEndBeforeEnd );
|
||||
}
|
||||
},
|
||||
|
||||
moveBoundariesDownTree: function () {
|
||||
var startContainer = this.startContainer,
|
||||
startOffset = this.startOffset,
|
||||
endContainer = this.endContainer,
|
||||
endOffset = this.endOffset,
|
||||
child;
|
||||
|
||||
while ( startContainer.nodeType !== TEXT_NODE ) {
|
||||
child = startContainer.childNodes[ startOffset ];
|
||||
if ( !child || child.nodeName === 'BR' ) {
|
||||
break;
|
||||
}
|
||||
startContainer = child;
|
||||
startOffset = 0;
|
||||
}
|
||||
if ( endOffset ) {
|
||||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||||
child = endContainer.childNodes[ endOffset - 1 ];
|
||||
if ( !child || child.nodeName === 'BR' ) {
|
||||
break;
|
||||
}
|
||||
endContainer = child;
|
||||
endOffset = endContainer.getLength();
|
||||
}
|
||||
} else {
|
||||
while ( endContainer.nodeType !== TEXT_NODE ) {
|
||||
child = endContainer.firstChild;
|
||||
if ( !child || child.nodeName === 'BR' ) {
|
||||
break;
|
||||
}
|
||||
endContainer = child;
|
||||
}
|
||||
}
|
||||
|
||||
// If collapsed, this algorithm finds the nearest text node positions
|
||||
// *outside* the range rather than inside, but also it flips which is
|
||||
// assigned to which.
|
||||
if ( this.collapsed ) {
|
||||
this.setStart( endContainer, endOffset );
|
||||
this.setEnd( startContainer, startOffset );
|
||||
} else {
|
||||
this.setStart( startContainer, startOffset );
|
||||
this.setEnd( endContainer, endOffset );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
moveBoundariesUpTree: function ( common ) {
|
||||
var startContainer = this.startContainer,
|
||||
startOffset = this.startOffset,
|
||||
endContainer = this.endContainer,
|
||||
endOffset = this.endOffset,
|
||||
parent;
|
||||
|
||||
if ( !common ) {
|
||||
common = this.commonAncestorContainer;
|
||||
}
|
||||
|
||||
while ( startContainer !== common && !startOffset ) {
|
||||
parent = startContainer.parentNode;
|
||||
startOffset = indexOf.call( parent.childNodes, startContainer );
|
||||
startContainer = parent;
|
||||
}
|
||||
|
||||
while ( endContainer !== common &&
|
||||
endOffset === endContainer.getLength() ) {
|
||||
parent = endContainer.parentNode;
|
||||
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
||||
endContainer = parent;
|
||||
}
|
||||
|
||||
this.setStart( startContainer, startOffset );
|
||||
this.setEnd( endContainer, endOffset );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getStartBlock: function () {
|
||||
var node = this.startContainer,
|
||||
offset = this.startOffset,
|
||||
children = node.childNodes;
|
||||
if ( node.nodeType === ELEMENT_NODE &&
|
||||
offset < children.length ) {
|
||||
node = children[ offset ];
|
||||
}
|
||||
if ( !node.isBlock() ) {
|
||||
node = node.getPreviousBlock() ||
|
||||
this.startContainer.ownerDocument.body.getNextBlock();
|
||||
}
|
||||
return node;
|
||||
},
|
||||
|
||||
getEndBlock: function () {
|
||||
var node = this.endContainer,
|
||||
offset = this.endOffset,
|
||||
children = node.childNodes;
|
||||
if ( node.nodeType === ELEMENT_NODE &&
|
||||
offset && offset <= children.length ) {
|
||||
node = children[ offset - 1 ];
|
||||
}
|
||||
if ( !node.isBlock() ) {
|
||||
node = node.getPreviousBlock() ||
|
||||
this.startContainer.ownerDocument.body.getNextBlock();
|
||||
}
|
||||
return node;
|
||||
},
|
||||
|
||||
startsAtBlockBoundary: function () {
|
||||
var startContainer = this.startContainer,
|
||||
startOffset = this.startOffset,
|
||||
parent, child;
|
||||
|
||||
while ( startContainer.isInline() ) {
|
||||
if ( startOffset ) {
|
||||
return false;
|
||||
}
|
||||
parent = startContainer.parentNode;
|
||||
startOffset = indexOf.call( parent.childNodes, startContainer );
|
||||
startContainer = parent;
|
||||
}
|
||||
// Skip empty text nodes and <br>s.
|
||||
while ( startOffset &&
|
||||
( child = startContainer.childNodes[ startOffset - 1 ] ) &&
|
||||
( child.data === '' || child.nodeName === 'BR' ) ) {
|
||||
startOffset -= 1;
|
||||
}
|
||||
return !startOffset;
|
||||
},
|
||||
|
||||
endsAtBlockBoundary: function () {
|
||||
var endContainer = this.endContainer,
|
||||
endOffset = this.endOffset,
|
||||
length = endContainer.getLength(),
|
||||
parent, child;
|
||||
|
||||
while ( endContainer.isInline() ) {
|
||||
if ( endOffset !== length ) {
|
||||
return false;
|
||||
}
|
||||
parent = endContainer.parentNode;
|
||||
endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
|
||||
endContainer = parent;
|
||||
length = endContainer.childNodes.length;
|
||||
}
|
||||
// Skip empty text nodes and <br>s.
|
||||
while ( endOffset < length &&
|
||||
( child = endContainer.childNodes[ endOffset ] ) &&
|
||||
( child.data === '' || child.nodeName === 'BR' ) ) {
|
||||
endOffset += 1;
|
||||
}
|
||||
return endOffset === length;
|
||||
},
|
||||
|
||||
expandToBlockBoundaries: function () {
|
||||
var start = this.getStartBlock(),
|
||||
end = this.getEndBlock(),
|
||||
parent = start.parentNode;
|
||||
|
||||
this.setStart( parent, indexOf.call( parent.childNodes, start ) );
|
||||
parent = end.parentNode;
|
||||
this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
}() );
|
57
source/document.html
Normal file
57
source/document.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
background: transparent;
|
||||
color: #2b2b2b;
|
||||
font: 13px/1.35 Helvetica, arial, sans-serif;
|
||||
cursor: text;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
h1 {
|
||||
font-size: 138.5%;
|
||||
}
|
||||
h2 {
|
||||
font-size: 123.1%;
|
||||
}
|
||||
h3 {
|
||||
font-size: 108%;
|
||||
}
|
||||
h1,h2,h3,p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
h4,h5,h6 {
|
||||
margin: 0;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 0 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid blue;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
<!--[if IE 9]>
|
||||
<script type="text/javascript">window.ie = 9;</script>
|
||||
<![endif]-->
|
||||
<script type="text/javascript" src="squire.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
Loading…
Reference in a new issue