scribe-plugin-span-style
Advanced tools
Comparing version 0.1.7 to 0.2.0
{ | ||
"name": "scribe-plugin-span-style", | ||
"version": "0.1.7", | ||
"version": "0.2.0", | ||
"description": "A Scribe Plugin for adding inline styles through <span> elements", | ||
@@ -5,0 +5,0 @@ "main": "src/scribe-plugin-span-style.js", |
# scribe-plugin-span-style | ||
A Scribe Plugin for adding inline styles through <span> elements | ||
A Scribe Plugin for adding inline styles through `<span>` elements. | ||
*The description `<span>` is probably not accurate anymore. From 0.2.0 on, the plugin will now use any available tags it can find, that includes `<b>`,`<i>`,`<strong>`,`<em>`,`<li>`,`etc`. Only if it is not able to use one of those will it create a `<span>` to wrap the selection into.* | ||
**Example:** | ||
@@ -21,6 +23,30 @@ ```javascript | ||
// ............... | ||
let command = scribe.getCommand('fontSize'); | ||
let originalRange = scribe.selection.range; | ||
// Some more code that causes the selection to lose focus... | ||
command.execute({ | ||
value: '24px', | ||
range: originalRange | ||
}); | ||
``` | ||
**Requirements** | ||
- Scribe should always put `<p>` or `<div>` tags as the root nodes of your lines | ||
**Known Issues / Todo** | ||
- Multi-Line selections do not get styled properly | ||
- Safari does not wrap the text into the spans correctly, scribe itself does not support Safari, so need to think about a workaround | ||
- Heavily nested `<span>` nodes sometimes do not get unwrapped properly | ||
- Untested performance on a lot of text ("logical" algorithm might be a bit slow for that) | ||
- Untested in older browsers | ||
**Changelog** | ||
*v0.2.0* | ||
- Complete code makeover | ||
- Multi-line selections are supported mostly reliable now (even cross browser) | ||
- Implemented "logical" algorithm to keep the nesting as clean as possible | ||
- Supports the passing of an object as value now (`{value: '12px', range: originalRange}`) to support input elements that might cause losing the selection before execution of the command |
@@ -9,52 +9,203 @@ 'use strict'; | ||
var clearChildStyles = function(root) { | ||
if (typeof root === 'undefined' || typeof root.childNodes === 'undefined') return; | ||
if (typeof root === 'undefined' || typeof root.childNodes === 'undefined' || typeof root.nodeType === 'undefined' || root.nodeType === 3) return; | ||
for (var i in root.childNodes) { | ||
var child = root.childNodes[i]; | ||
clearChildStyles(root.childNodes[i]); | ||
} | ||
clearChildStyles(child.childNodes); | ||
root.style[styleName] = ''; | ||
if (root.nodeName === 'SPAN') { | ||
if (!root.getAttribute('style')) { | ||
scribe.node.unwrap(root.parentNode, root); | ||
} | ||
} | ||
}; | ||
if (child.nodeName === 'SPAN') { | ||
child.style[styleName] = ''; | ||
if (!child.getAttribute('style')) { | ||
scribe.node.unwrap(root, child); | ||
} | ||
var nextNode = function(node) { | ||
if (node.hasChildNodes()) { | ||
return node.firstChild; | ||
} | ||
else { | ||
while (node && !node.nextSibling) { | ||
node = node.parentNode; | ||
} | ||
if (!node) { | ||
return null; | ||
} | ||
return node.nextSibling; | ||
} | ||
}; | ||
var getRangeSelectedNodes = function(range) { | ||
var node = range.startContainer; | ||
var endNode = range.endContainer; | ||
if (node === endNode) { | ||
return [node]; | ||
} | ||
var rangeNodes = []; | ||
while (node && node !== endNode) { | ||
rangeNodes.push(node = nextNode(node)); | ||
} | ||
node = range.startContainer; | ||
while (node && node !== range.commonAncestorContainer) { | ||
rangeNodes.unshift(node); | ||
node = node.parentNode; | ||
} | ||
return rangeNodes; | ||
}; | ||
var getLogicalCombinations = function(fragments) { | ||
var p = []; | ||
for (var i=0;i<fragments.length;i++) { | ||
var f = fragments[i]; | ||
p.push(f); | ||
for (var x = i+1;x<fragments.length;x++) { | ||
f += fragments[x]; | ||
p.push(f); | ||
} | ||
} | ||
return p; | ||
}; | ||
var styleLogicalFragments = function(root, fragments, value) { | ||
if (typeof root === 'undefined' || root.nodeType === 3) return; | ||
if (fragments.indexOf(root.textContent) > -1) { | ||
clearChildStyles(root); | ||
root.style[styleName] = value; | ||
} | ||
else if (root.childNodes) { | ||
for (var i in root.childNodes) { | ||
styleLogicalFragments(root.childNodes[i], fragments, value); | ||
} | ||
} | ||
}; | ||
var normalizeNodes = function(root) { | ||
if (typeof root === 'undefined' || root.nodeType === 3 || typeof root.nodeType === 'undefined') return; | ||
if (root.childNodes) { | ||
for (var i in root.childNodes) { | ||
normalizeNodes(root.childNodes[i]); | ||
} | ||
} | ||
root.normalize(); | ||
}; | ||
var createSubRange = function(node, start, end) { | ||
var subRange = document.createRange(); | ||
subRange.setStart(node, start); | ||
subRange.setEnd(node, end); | ||
return subRange; | ||
}; | ||
spanStyleCommand.execute = function(value) { | ||
var selection = new scribe.api.Selection(); | ||
var range = selection.range; | ||
if (typeof value.range !== 'undefined') { | ||
// Sometimes a click on the firing element (button etc.) causes the selection to lose focus | ||
// If needed a range from the original selection can be passed alongside the value | ||
range = value.range; | ||
value = value.value; | ||
} | ||
if (range.collapsed) { | ||
// Trying to style a collapsed range makes no sense | ||
return; | ||
} | ||
scribe.transactionManager.run(function() { | ||
var selection = new scribe.api.Selection(); | ||
var range = selection.range; | ||
var nodes = getRangeSelectedNodes(range); | ||
// Get Range Text & Current Node Text | ||
var selectedHtmlDocumentFragment = range.extractContents(); | ||
var tDiv = document.createElement('div'); | ||
tDiv.appendChild(selectedHtmlDocumentFragment.cloneNode(true)); | ||
var rangeText = tDiv.innerText; | ||
var nodeText = selection.selection.focusNode.textContent; | ||
if (nodes.length === 1) { | ||
var node = nodes[0]; | ||
var parent = node.parentNode; | ||
if (node.nodeType === 3) { | ||
if (parent.textContent === range.startContainer.textContent.substr(range.startOffset,range.endOffset)) { | ||
clearChildStyles(parent); | ||
parent.style[styleName] = value; | ||
} | ||
else { | ||
var span = document.createElement('span'); | ||
span.style[styleName] = value; | ||
// Determine if we need a new node | ||
var isNewNode = true; | ||
if (nodeText === rangeText) { | ||
isNewNode = (selection.selection.focusNode.parentElement.nodeName === 'SPAN') ? false : true; | ||
range.surroundContents(span); | ||
} | ||
} | ||
} | ||
else { | ||
var startOffset = range.startOffset; | ||
var startContainer = range.startContainer; | ||
var endOffset = range.endOffset; | ||
var endContainer = range.endContainer; | ||
// Create / Get SPAN | ||
var span = (!isNewNode) ? selection.selection.focusNode.parentElement : document.createElement('span'); | ||
span.appendChild(selectedHtmlDocumentFragment); | ||
if (isNewNode) { | ||
range.insertNode(span); | ||
range.selectNode(span); | ||
var fragments = []; | ||
for (var i in nodes) { | ||
var node = nodes[i]; | ||
if (node.nodeType === 3) { | ||
if (node === range.startContainer) { | ||
fragments.push(node.textContent.substr(range.startOffset)); | ||
} | ||
else if (node === range.endContainer) { | ||
fragments.push(node.textContent.substr(0,range.endOffset)); | ||
} | ||
else { | ||
fragments.push(node.textContent); | ||
} | ||
} | ||
} | ||
fragments = getLogicalCombinations(fragments); | ||
if (scribe.el.childNodes) { | ||
for (var i in scribe.el.childNodes) { | ||
var rootNode = scribe.el.childNodes[i]; | ||
styleLogicalFragments(rootNode, fragments, value); | ||
} | ||
} | ||
if (startOffset > 0) { | ||
var parent = startContainer.parentNode; | ||
if (parent.innerText === startContainer.textContent.substr(startOffset)) { | ||
clearChildStyles(parent); | ||
parent.style[styleName] = value; | ||
} | ||
else { | ||
var subRange = createSubRange(startContainer, startOffset, startContainer.textContent.length); | ||
var span = document.createElement('span'); | ||
span.style[styleName] = value; | ||
subRange.surroundContents(span); | ||
} | ||
} | ||
if (endOffset < endContainer.textContent.length) { | ||
var parent = endContainer.parentNode; | ||
if (parent.innerText === endContainer.textContent.substr(0,endOffset)) { | ||
clearChildStyles(parent); | ||
parent.style[styleName] = value; | ||
} | ||
else { | ||
var subRange = createSubRange(endContainer, 0, endOffset); | ||
var span = document.createElement('span'); | ||
span.style[styleName] = value; | ||
subRange.surroundContents(span); | ||
} | ||
} | ||
} | ||
// Clear Setting for children | ||
clearChildStyles(span); | ||
// Apply new Font-Size | ||
span.style[styleName] = value; | ||
// Re-apply the range | ||
selection.selection.removeAllRanges(); | ||
selection.selection.addRange(range); | ||
if (scribe.el.childNodes) { | ||
normalizeNodes(scribe.el); | ||
} | ||
}); | ||
@@ -65,6 +216,5 @@ }; | ||
var selection = new scribe.api.Selection(); | ||
var that = this; | ||
return !!selection.getContaining(function(node) { | ||
if (node.style) { | ||
return (node.nodeName === that.nodeName && node.style[styleName] !== ''); | ||
return (node.style[styleName] !== ''); | ||
} | ||
@@ -71,0 +221,0 @@ return false; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
10844
187
51