Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

medium-editor

Package Overview
Dependencies
Maintainers
4
Versions
125
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

medium-editor - npm Package Compare versions

Comparing version 5.0.0-alpha.0 to 5.0.0-rc.1

4

bower.json
{
"name": "medium-editor",
"version": "5.0.0-alpha.0",
"homepage": "http://yabwe.github.io/medium-editor/",

@@ -8,3 +7,4 @@ "authors": [

"Nate Mielnik <nathan@outlook.com>",
"Noah Chase <nchase@gmail.com>"
"Noah Chase <nchase@gmail.com>",
"Jeremy Benoist <jeremy.benoist@gmail.com>"
],

@@ -11,0 +11,0 @@ "description": "Medium.com WYSIWYG editor clone written in pure JavaScript.",

@@ -295,3 +295,3 @@ /*global module, require, process*/

options: {
files: ['bower.json', 'package.json', 'src/js/version.js'],
files: ['package.json', 'src/js/version.js'],
updateConfigs: [],

@@ -298,0 +298,0 @@ commit: false,

{
"name": "medium-editor",
"version": "5.0.0-alpha.0",
"version": "5.0.0-rc.1",
"author": "Davi Ferreira <hi@daviferreira.com>",

@@ -5,0 +5,0 @@ "contributors": [

@@ -91,8 +91,26 @@ # MediumEditor

## Initialization options
##### Integrating with various frameworks
People have contributed wrappers around MediumEditor for integrating with different frameworks and tech stacks. Take a look at the list of existing [Wrappers and Integrations](https://github.com/yabwe/medium-editor/wiki/Wrappers-and-Integration) that have already been written for MediumEditor!
## MediumEditor Options
View the [MediumEditor Options documentation](https://github.com/yabwe/medium-editor/wiki/Options) on the Wiki for details on all the various options for MediumEditor.
Options to customize medium-editor are passed as the second argument to the [MediumEditor constructor](https://github.com/yabwe/medium-editor/wiki/MediumEditor-Object-API#mediumeditorelements-options). Example:
```js
var editor = new MediumEditor('.editor', {
// options go here
});
```
### Core options
* __activeButtonClass__: CSS class added to active buttons in the toolbar. Default: `'medium-editor-button-active'`
* __allowMultiParagraphSelection__: enables the toolbar when selecting multiple paragraphs/block elements. Default: `true`
* __buttonLabels__: type of labels on the buttons. Values: 'fontawesome', `{'bold': '<b>b</b>', 'italic': '<i>i</i>'}`. Default: `false`
* __buttonLabels__: type of labels on the buttons. Values: `false` | 'fontawesome'. Default: `false`
#### NOTE:
Using `'fontawesome'` as the buttonLabels requires version 4.1.0 of the fontawesome css to be on the page to ensure all icons will be displayed correctly
* __delay__: time in milliseconds to show the toolbar or anchor tag preview. Default: `0`

@@ -104,4 +122,2 @@ * __disableReturn__: enables/disables the use of the return-key. You can also set specific element behavior by using setting a data-disable-return attribute. Default: `false`

* __extensions__: extension to use (see [Custom Buttons and Extensions](https://github.com/yabwe/medium-editor/wiki/Custom-Buttons-and-Extensions)) for more. Default: `{}`
* __firstHeader__: HTML tag to be used as first header. Default: `h3`
* __secondHeader__: HTML tag to be used as second header. Default: `h4`
* __spellcheck__: Enable/disable native contentEditable automatic spellcheck. Default: `true`

@@ -120,3 +136,3 @@ * __targetBlank__: enables/disables target="\_blank" for anchor tags. Default: `false`

if nothing is passed this is what is used */
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
diffLeft: 0,

@@ -137,3 +153,4 @@ diffTop: -10,

* __buttons__: the set of buttons to display on the toolbar. Default: `['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote']`
* __buttons__: the set of buttons to display on the toolbar. Default: `['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']`
* See [Button Options](#button-options) for details on more button options
* __diffLeft__: value in pixels to be added to the X axis positioning of the toolbar. Default: `0`

@@ -158,2 +175,62 @@ * __diffTop__: value in pixels to be added to the Y axis positioning of the toolbar. Default: `-10`

#### Button Options
Button behavior can be modified by passing an object into the buttons array instead of a string. This allow for overriding some of the default behavior of buttons. The following options are some of the basic parts of buttons that you may override, but any part of the `MediumEditor.extension.prototype` can be overriden via these button options. (Check out the [source code for buttons](https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/button.js) to see what all can be overriden).
* __name__: name of the button being overriden
* __action__: argument to pass to `MediumEditor.execAction()` when the button is clicked.
* __aria__: value to add as the aria-label attribute of the button element displayed in the toolbar. This is also used as the tooltip for the button.
* __tagNames__: array of element tag names that would indicate that this button has already been applied. If this action has already been applied, the button will be displayed as 'active' in the toolbar.
* _Example_: For 'bold', if the text is ever within a `<b>` or `<strong>` tag that indicates the text is already bold. So the array of tagNames for bold would be: `['b', 'strong']`
* __NOTE__: This is not used if `useQueryState` is set to `true`.
* __style__: A pair of css property & value(s) that indicate that this button has already been applied. If this action has already been applied, the button will be displayed as 'active' in the toolbar.
* _Example_: For 'bold', if the text is ever within an element with a `'font-weight'` style property set to `700` or `'bold'`, that indicates the text is already bold. So the style object for bold would be `{ prop: 'font-weight', value: '700|bold' }`
* __NOTE__: This is not used if `useQueryState` is set to `true`.
* Properties of the __style__ object:
* __prop__: name of the css property
* __value__: value(s) of the css property (multiple values can be separated by a `'|'`)
* __useQueryState__: Enables/disables whether this button should use the built-in `document.queryCommandState()` method to determine whether the action has already been applied. If the action has already been applied, the button will be displayed as 'active' in the toolbar
* _Example_: For 'bold', if this is set to true, the code will call `document.queryCommandState('bold')` which will return true if the browser thinks the text is already bold, and false otherwise
* __contentDefault__: Default `innerHTML` to put inside the button
* __contentFA__: The `innerHTML` to use for the content of the button if the __buttonLabels__ option for MediumEditor is set to `'fontawesome'`
* __classList__: An array of classNames (strings) to be added to the button
* __attrs__: A set of key-value pairs to add to the button as custom attributes to the button element.
Example of overriding buttons (here, the goal is to mimic medium by having <kbd>H1</kbd> and <kbd>H2</kbd> buttons which actually produce `<h2>` and `<h3>` tags respectively):
```javascript
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: [
'bold',
'italic',
{
name: 'h1',
action: 'append-h2',
aria: 'header type 1',
tagNames: ['h2'],
contentDefault: '<b>H1</b>',
classList: ['custom-class-h1'],
attrs: {
'data-custom-attr': 'attr-value-h1'
}
},
{
name: 'h2',
action: 'append-h3',
aria: 'header type 2',
tagNames: ['h3'],
contentDefault: '<b>H2</b>',
classList: ['custom-class-h2'],
attrs: {
'data-custom-attr': 'attr-value-h2'
}
},
'justifyCenter',
'quote',
'anchor'
]
}
});
```
### Anchor Preview options

@@ -282,3 +359,3 @@

command: 'bold',
key: 'b',
key: 'B',
meta: true,

@@ -289,3 +366,3 @@ shift: false

command: 'italic',
key: 'i',
key: 'I',
meta: true,

@@ -296,3 +373,3 @@ shift: false

command: 'underline',
key: 'u',
key: 'U',
meta: true,

@@ -308,2 +385,3 @@ shift: false

* _command_: argument passed to `editor.execAction()` when key-combination is used
* if defined as `false`, the shortcut will be disabled
* _key_: keyboard character that triggers this command

@@ -347,4 +425,2 @@ * _meta_: whether the ctrl/meta key has to be active or inactive

var editor = new MediumEditor('.editable', {
firstHeader: 'h1',
secondHeader: 'h2',
delay: 1000,

@@ -376,22 +452,49 @@ targetBlank: true,

## Extra buttons
Medium Editor, by default, will show only the buttons listed above to avoid a huge toolbar. There are a few extra buttons you can use:
## Buttons
By default, MediumEditor supports buttons for most of the commands for `document.execCommand()` that are well-supported across all its supported browsers.
### Default buttons.
MediumEditor, by default, will show only the buttons listed here to avoid a huge toolbar:
* __bold__
* __italic__
* __underline__
* __anchor__ _(built-in support for collecting a url via the anchor extension)_
* __h2__
* __h3__
* __quote__
### All buttons.
These are all the built-in buttons supported by MediumEditor.
* __bold__
* __italic__
* __underline__
* __strikethrough__
* __subscript__
* __superscript__
* __subscript__
* __strikethrough__
* __anchor__
* __image__ (this simply converts selected text to an image tag)
* __quote__
* __pre__
* __orderedlist__
* __unorderedlist__
* __orderedlist__
* __pre__
* __indent__ (moves the selected text up one level)
* __outdent__ (moves the selected text down one level)
* __justifyLeft__
* __justifyFull__
* __justifyCenter__
* __justifyRight__
* __image__ (this simply converts selected text to an image tag)
* __indent__ (moves the selected text up one level)
* __outdent__ (moves the selected text down one level)
* __justifyFull__
* __h1__
* __h2__
* __h3__
* __h4__
* __h5__
* __h6__
* __removeFormat__ (clears inline style formatting, preserves blocks)
## Themes

@@ -403,32 +506,44 @@

### Core Methods
View the [MediumEditor Object API documentation](https://github.com/yabwe/medium-editor/wiki/MediumEditor-Object-API) on the Wiki for details on all the methods supported on the MediumEditor object.
### Initialization methods
* __MediumEditor(elements, options)__: Creates an instance of MediumEditor
* __.destroy()__: tears down the editor if already setup, removing all DOM elements and event handlers
* __.setup()__: rebuilds the editor if it has already been destroyed, recreating DOM elements and attaching event handlers
* __.serialize()__: returns a JSON object with elements contents
* __.execAction(action, opts)__: executes an built-in action via `document.execCommand`
* __.createLink(opts)__: creates a link via the native `document.execCommand('createLink')` command
### Event Methods
* __.on(target, event, listener, useCapture)__: attach a listener to a DOM event which will be detached when MediumEditor is deactivated
* __.off(target, event, listener, useCapture)__: detach a listener to a DOM event that was attached via `on()`
* __.subscribe(event, listener)__: attaches a listener to a custom medium-editor event
* __.unsubscribe(event, listener)__: detaches a listener from a custom medium-editor event
* __.trigger(name, data, editable)__: manually triggers a custom medium-editor event
### Selection Methods
* __.checkSelection()__: manually trigger an update of the toolbar and extensions based on the current selection
* __.exportSelection()__: return a data representation of the selected text, which can be applied via `importSelection()`
* __.importSelection(selectionState)__: restore the selection using a data representation of previously selected text (ie value returned by `exportSelection()`)
* __.getFocusedElement()__: returns an element if any contenteditable element monitored by MediumEditor currently has focused
* __.getSelectionParentElement(range)__: get the parent contenteditable element that contains the current selection
* __.restoreSelection()__: restore the selection to what was selected when `saveSelection()` was called
* __.saveSelection()__: internally store the set of selected text
* __.restoreSelection()__: restore the selection to what was selected when `saveSelection()` was called
* __.selectAllContents()__: expands the selection to contain all text within the focused contenteditable
* __.selectElement(element)__: change selection to be a specific element and update the toolbar to reflect the selection
* __.stopSelectionUpdates()__: stop the toolbar from updating to reflect the state of the selected text
* __.startSelectionUpdates()__: put the toolbar back into its normal updating state
### Editor Action Methods
* __.cleanPaste(text)__: convert text to plaintext and replace current selection with result
* __.createLink(opts)__: creates a link via the native `document.execCommand('createLink')` command
* __.execAction(action, opts)__: executes an built-in action via `document.execCommand`
* __.pasteHTML(html, options)__: replace the current selection with html
* __.queryCommandState(action)__: wrapper around the browser's built in `document.queryCommandState(action)` for checking whether a specific action has already been applied to the selection.
### Helper Methods
* __.on(target, event, listener, useCapture)__: attach a listener to a DOM event which will be detached when MediumEditor is deactivated
* __.off(target, event, listener, useCapture)__: detach a listener to a DOM event that was attached via `on()`
* __.delay(fn)__: delay any function from being executed by the amount of time passed as the `delay` option
* __.getSelectionParentElement(range)__: get the parent contenteditable element that contains the current selection
* __.getExtensionByName(name)__: get a reference to an extension with the specified name
* __.getFocusedElement()__: returns an element if any contenteditable element monitored by MediumEditor currently has focused
* __.selectElement(element)__: change selection to be a specific element and update the toolbar to reflect the selection
* __.exportSelection()__: return a data representation of the selected text, which can be applied via `importSelection()`
* __.importSelection(selectionState)__: restore the selection using a data representation of previously selected text (ie value returned by `exportSelection()`)
* __.serialize()__: returns a JSON object with elements contents
## Capturing DOM changes
For observing any changes on contentEditable, use the custom 'editableInput' event exposed via the `subscribe()` method:
For observing any changes on contentEditable, use the custom `'editableInput'` event exposed via the `subscribe()` method:

@@ -435,0 +550,0 @@ ```js

@@ -42,3 +42,3 @@ /*global MediumEditor, describe, it, expect, spyOn,

toolbar = editor.getExtensionByName('toolbar'),
code = 'k'.charCodeAt(0);
code = 'K'.charCodeAt(0);

@@ -45,0 +45,0 @@ selectElementContentsAndFire(editor.elements[0]);

@@ -186,3 +186,3 @@ /*global describe, it, expect, beforeEach, afterEach,

links = this.el.getElementsByTagName('a');
expect(links.length).toBe(1);
expect(links.length).toBe(1, 'links length after ENTER');
expect(links[0].getAttribute('href')).toBe('http://www.example.enter');

@@ -197,3 +197,3 @@ expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');

links = this.el.getElementsByTagName('a');
expect(links.length).toBe(1);
expect(links.length).toBe(1, 'links length after SPACE');
expect(links[0].getAttribute('href')).toBe('http://www.example.space');

@@ -240,3 +240,3 @@ expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');

expect(links.length).toBe(1);
expect(this.el.firstChild.tagName.toLowerCase()).toBe('span');
expect(this.el.firstChild.nodeName.toLowerCase()).toBe('span');
expect(this.el.firstChild.textContent).toBe('Text with http://www.example.com inside!');

@@ -243,0 +243,0 @@ expect(this.el.firstChild.getElementsByTagName('a').length).toBe(1);

/*global MediumEditor, describe, it, expect, spyOn, AnchorForm,
beforeAll, afterAll,
afterEach, beforeEach, jasmine, fireEvent, setupTestHelpers,

@@ -8,2 +9,14 @@ selectElementContentsAndFire, isOldIE, isIE */

var textarea;
beforeAll(function () {
textarea = document.createElement('textarea');
textarea.innerHTML = 'Ignore me please, placed here to make create an image test pass in Gecko';
document.body.appendChild(textarea);
textarea.focus();
});
afterAll(function () {
document.body.removeChild(textarea);
});
beforeEach(function () {

@@ -98,2 +111,67 @@ setupTestHelpers.call(this);

describe('Button options', function () {
it('should support overriding defaults', function () {
this.el.innerHTML = '<h2>lorem</h2><h3>ipsum</h3>';
var editor = this.newMediumEditor('.editor', {
toolbar: {
buttons: [
'bold',
{
name: 'h1',
action: 'append-h2',
aria: 'fake h1',
tagNames: ['h2'],
contentDefault: '<b>H1</b>',
classList: ['customClassName'],
attrs: {
'data-custom-attr': 'custom-value'
}
},
{
name: 'h2',
getAction: function () {
return 'append-h3';
},
getAria: function () {
return 'fake h2';
},
getTagNames: function () {
return ['h3'];
},
contentDefault: '<b>H2</b>'
}
]
}
}),
headerOneButton = editor.getExtensionByName('h1'),
headerTwoButton = editor.getExtensionByName('h2'),
toolbar = editor.getExtensionByName('toolbar');
expect(toolbar.getToolbarElement().querySelectorAll('button').length).toBe(3);
var button = toolbar.getToolbarElement().querySelector('.medium-editor-action-h1'),
buttonTwo = toolbar.getToolbarElement().querySelector('.medium-editor-action-h2');
expect(button).toBe(headerOneButton.getButton());
expect(button.getAttribute('aria-label')).toBe('fake h1');
expect(button.getAttribute('title')).toBe('fake h1');
expect(button.getAttribute('data-custom-attr')).toBe('custom-value');
expect(button.classList.contains('customClassName')).toBe(true);
expect(button.innerHTML).toBe('<b>H1</b>');
selectElementContentsAndFire(editor.elements[0].querySelector('h2').firstChild);
jasmine.clock().tick(1);
expect(button.classList.contains('medium-editor-button-active')).toBe(true);
expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(false);
expect(buttonTwo).toBe(headerTwoButton.getButton());
expect(buttonTwo.getAttribute('aria-label')).toBe('fake h2');
expect(buttonTwo.getAttribute('title')).toBe('fake h2');
expect(buttonTwo.innerHTML).toBe('<b>H2</b>');
selectElementContentsAndFire(editor.elements[0].querySelector('h3'), { eventToFire: 'mouseup' });
expect(button.classList.contains('medium-editor-button-active')).toBe(false);
expect(buttonTwo.classList.contains('medium-editor-button-active')).toBe(true);
});
});
describe('Buttons with various labels', function () {

@@ -104,2 +182,3 @@ var defaultLabels = {},

allButtons = [],
customButtons = [],
buttonsData = MediumEditor.extensions.button.prototype.defaults,

@@ -110,24 +189,55 @@ currButton,

Object.keys(buttonsData).forEach(function (buttonName) {
if (buttonName !== 'header1' && buttonName !== 'header2') {
allButtons.push(buttonName);
currButton = buttonsData[buttonName];
// If the labels contain HTML entities, we need to escape them
tempEl = document.createElement('div');
allButtons.push(buttonName);
currButton = buttonsData[buttonName];
// If the labels contain HTML entities, we need to escape them
tempEl = document.createElement('div');
// Default Labels
tempEl.innerHTML = currButton.contentDefault;
defaultLabels[buttonName] = {
action: currButton.action,
label: tempEl.innerHTML
};
// Default Labels
tempEl.innerHTML = currButton.contentDefault;
defaultLabels[buttonName] = {
action: currButton.action,
label: tempEl.innerHTML
};
// fontawesome labels
tempEl.innerHTML = currButton.contentFA;
fontAwesomeLabels[buttonName] = tempEl.innerHTML;
// fontawesome labels
tempEl.innerHTML = currButton.contentFA;
fontAwesomeLabels[buttonName] = tempEl.innerHTML;
// custom labels (using aria label as a test)
customLabels[buttonName] = currButton.aria;
}
// custom labels (using aria label as a test)
customLabels[buttonName] = currButton.aria;
customButtons.push({
name: buttonName,
contentDefault: currButton.aria
});
});
// Add in anchor button
allButtons.push('anchor');
tempEl = document.createElement('div');
tempEl.innerHTML = MediumEditor.extensions.anchor.prototype.contentDefault;
defaultLabels['anchor'] = {
label: tempEl.innerHTML
};
tempEl.innerHTML = MediumEditor.extensions.anchor.prototype.contentFA;
fontAwesomeLabels['anchor'] = tempEl.innerHTML;
customLabels['anchor'] = MediumEditor.extensions.anchor.prototype.aria;
customButtons.push({
name: 'anchor',
contentDefault: customLabels['anchor']
});
// Add in fontsize button
allButtons.push('fontsize');
tempEl.innerHTML = MediumEditor.extensions.fontSize.prototype.contentDefault;
defaultLabels['fontsize'] = {
label: tempEl.innerHTML
};
tempEl.innerHTML = MediumEditor.extensions.fontSize.prototype.contentFA;
fontAwesomeLabels['fontsize'] = tempEl.innerHTML;
customLabels['fontsize'] = MediumEditor.extensions.fontSize.prototype.aria;
customButtons.push({
name: 'fontsize',
contentDefault: customLabels['fontsize']
});
it('should have aria-label and title attributes set', function () {

@@ -188,3 +298,5 @@ var button,

fireEvent(button, 'click');
expect(editor.execAction).toHaveBeenCalledWith(action);
if (action) {
expect(editor.execAction).toHaveBeenCalledWith(action);
}
expect(button.innerHTML).toBe(fontAwesomeLabels[buttonName]);

@@ -200,5 +312,4 @@ });

toolbar: {
buttons: allButtons
},
buttonLabels: customLabels
buttons: customButtons
}
}),

@@ -215,3 +326,5 @@ toolbar = editor.getExtensionByName('toolbar');

fireEvent(button, 'click');
expect(editor.execAction).toHaveBeenCalledWith(action);
if (action) {
expect(editor.execAction).toHaveBeenCalledWith(action);
}
expect(button.innerHTML).toBe(customLabels[buttonName]);

@@ -239,3 +352,3 @@ });

it('should create an h3 element when header1 is clicked', function () {
it('should create an h3 element when h3 is clicked', function () {
this.el.innerHTML = '<p><b>lorem ipsum</b></p>';

@@ -856,3 +969,3 @@ var button,

toolbar: {
buttons: ['header1', 'header2']
buttons: ['h3', 'h4']
}

@@ -881,6 +994,4 @@ }),

toolbar: {
buttons: ['header1', 'header2']
},
firstHeader: 'h1',
secondHeader: 'h5'
buttons: ['h1', 'h5']
}
}),

@@ -911,5 +1022,4 @@ toolbar = editor.getExtensionByName('toolbar'),

toolbar: {
buttons: ['header1', 'header2']
},
firstHeader: 'h1'
buttons: ['h1', 'h4']
}
}),

@@ -916,0 +1026,0 @@ toolbar = editor.getExtensionByName('toolbar'),

@@ -304,3 +304,3 @@ /*global describe, it, expect, spyOn,

range = document.getSelection().getRangeAt(0);
expect(range.commonAncestorContainer.tagName.toLowerCase()).toBe('p');
expect(range.commonAncestorContainer.nodeName.toLowerCase()).toBe('p');
});

@@ -307,0 +307,0 @@

@@ -13,3 +13,3 @@ /*global describe, it, expect, spyOn,

var child = el.childNodes[0];
expect(child.tagName).toBe('FONT');
expect(child.nodeName.toLowerCase()).toBe('font');
expect(child.getAttribute('size')).toBe(size);

@@ -16,0 +16,0 @@ expect(child.innerHTML).toBe('lorem ipsum');

@@ -37,3 +37,3 @@ /*global describe, it, expect, afterEach, Util,

expect(el).toBeDefined();
expect(el.tagName).toBe('H2');
expect(el.nodeName.toLowerCase()).toBe('h2');
});

@@ -58,3 +58,3 @@

el = document.getElementById('header');
expect(el.previousElementSibling.tagName).toBe('P');
expect(el.previousElementSibling.nodeName.toLowerCase()).toBe('p');

@@ -76,3 +76,3 @@ });

// hit backspace
fireEvent(editor.elements[0].querySelector(el.tagName.toLowerCase()), 'keydown', {
fireEvent(editor.elements[0].querySelector(el.nodeName.toLowerCase()), 'keydown', {
keyCode: Util.keyCode.BACKSPACE

@@ -83,3 +83,3 @@ });

expect(el).toBeDefined();
expect(el.tagName).toBe('H2');
expect(el.nodeName.toLowerCase()).toBe('h2');

@@ -86,0 +86,0 @@ el = document.getElementById('editor');

@@ -110,5 +110,3 @@ /*global MediumEditor, describe, it, expect, spyOn,

ownerDocument: document,
firstHeader: 'h3',
allowMultiParagraphSelection: true,
secondHeader: 'h4',
buttonLabels: false,

@@ -127,4 +125,2 @@ targetBlank: false,

var options = {
firstHeader: 'h2',
secondHeader: 'h3',
delay: 300,

@@ -131,0 +127,0 @@ toolbar: {

@@ -25,3 +25,3 @@ /*global MediumEditor, describe, it, expect, beforeEach,

fireEvent(editor.elements[0], 'keydown', {
keyCode: 'b'.charCodeAt(0),
keyCode: 'B'.charCodeAt(0),
ctrlKey: true,

@@ -34,3 +34,3 @@ metaKey: true

fireEvent(editor.elements[0], 'keydown', {
keyCode: 'i'.charCodeAt(0),
keyCode: 'I'.charCodeAt(0),
ctrlKey: true,

@@ -43,3 +43,3 @@ metaKey: true

fireEvent(editor.elements[0], 'keydown', {
keyCode: 'u'.charCodeAt(0),
keyCode: 'U'.charCodeAt(0),
ctrlKey: true,

@@ -94,3 +94,3 @@ metaKey: true

fireEvent(editor.elements[0], 'keydown', {
keyCode: 'b'.charCodeAt(0),
keyCode: 'B'.charCodeAt(0),
ctrlKey: true,

@@ -111,3 +111,3 @@ metaKey: true,

fireEvent(editor.elements[0], 'keydown', {
keyCode: 'b'.charCodeAt(0),
keyCode: 'B'.charCodeAt(0),
ctrlKey: true,

@@ -118,3 +118,38 @@ metaKey: true

});
it('should not execute when the command key are false', function () {
spyOn(MediumEditor.prototype, 'execAction');
var result,
editor = this.newMediumEditor('.editor', {
keyboardCommands: {
commands: [
{
command: false,
key: 'J',
meta: true,
shift: false
}
]
}
});
selectElementContentsAndFire(editor.elements[0]);
jasmine.clock().tick(1);
fireEvent(editor.elements[0], 'keydown', {
keyCode: 'J'.charCodeAt(0),
ctrlKey: true,
metaKey: true,
shiftKey: false
});
expect(editor.execAction).not.toHaveBeenCalled();
result = fireEvent(editor.elements[0], 'keydown', {
keyCode: 'J'.charCodeAt(0),
ctrlKey: true,
metaKey: true,
shiftKey: true
});
expect(result).toBe(false, 'The command was not blocked because shift key was pressed');
});
});
});
});
/*global MediumEditor, describe, it, expect, spyOn,
afterEach, beforeEach, fireEvent,
afterEach, beforeEach, fireEvent, Util,
jasmine, selectElementContents, setupTestHelpers,

@@ -84,2 +84,228 @@ selectElementContentsAndFire, Selection,

});
it('should not export a position indicating the cursor is before an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
placeCursorInsideElement(editor.elements[0].querySelector('span'), 1); // end of first span
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);
});
it('should not export a position indicating the cursor is after an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +
'<p class="target">Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
// After the 'W' in whatever
placeCursorInsideElement(editor.elements[0].querySelector('p.target').firstChild, 1);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);
});
it('should not export a position indicating the cursor is after an empty paragraph (in a complicated markup case)',
function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +
'<p>What<span class="target">ever</span></p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
// Before the 'e' in whatever
placeCursorInsideElement(editor.elements[0].querySelector('span.target').firstChild, 0);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);
});
it('should not export a position indicating the cursor is after an empty paragraph ' +
'(in a complicated markup with selection on the element)', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' +
'<p>What<span class="target">ever</span></p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
// Before the 'e' in whatever
placeCursorInsideElement(editor.elements[0].querySelector('span.target'), 0);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);
});
it('should export a position indicating the cursor is in an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[1], 0);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(1);
});
it('should export a position indicating the cursor is after an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[2], 0);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(2);
});
it('should export a position indicating the cursor is after an empty block element', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
placeCursorInsideElement(editor.elements[0].querySelector('h2'), 0);
var exportedSelection = editor.exportSelection();
expect(exportedSelection.emptyBlocksIndex).toEqual(2);
});
it('should import a position with the cursor in an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 1
});
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[1], 'empty paragraph');
});
it('should import a position with the cursor after an empty paragraph', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
});
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[2], 'paragraph after empty paragraph');
});
it('should import a position with the cursor after an empty paragraph when there are multipled editable elements', function () {
this.createElement('div', 'editor', '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>');
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'editableElementIndex': 1,
'emptyBlocksIndex': 2
});
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(editor.elements[1].getElementsByTagName('p')[2], 'paragraph after empty paragraph');
});
it('should import a position with the cursor after an empty block element', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
});
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');
expect(startParagraph).toBe(editor.elements[0].querySelector('h2'), 'block element after empty block element');
});
it('should import a position with the cursor after an empty block element when there are nested block elements', function () {
this.el.innerHTML = '<blockquote><p><span>www.google.com</span></p></blockquote><h1><br /></h1><h2><br /></h2><p>Whatever</p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
});
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');
expect(startParagraph).toBe(editor.elements[0].querySelector('h2'), 'block element after empty block element');
});
it('should import a position with the cursor after an empty block element inside an element with various children', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p><b><i>Whatever</i></b></p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 3
});
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(Util.isDescendant(editor.elements[0].querySelector('i'), innerElement, true)).toBe(true, 'nested inline elment inside block element after empty block element');
});
['br', 'img'].forEach(function (tagName) {
it('should not import a selection into focusing on the element \'' + tagName + '\' that cannot have children', function () {
this.el.innerHTML = '<p>Hello</p><p><' + tagName + ' /></p><p>World<p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 5,
'end': 5,
'emptyBlocksIndex': 1
});
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(innerElement.nodeName.toLowerCase()).toBe('p', 'focused element nodeName');
expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);
expect(innerElement.previousSibling.nodeName.toLowerCase()).toBe('p', 'previous sibling name');
expect(innerElement.nextSibling.nodeName.toLowerCase()).toBe('p', 'next sibling name');
});
});
it('should not import a selection into focusing on an empty element in a table', function () {
this.el.innerHTML = '<p>Hello</p><table><colgroup><col /></colgroup>' +
'<thead><tr><th>Head</th></tr></thead>' +
'<tbody><tr><td>Body</td></tr></tbody></table><p>World<p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
editor.importSelection({
'start': 5,
'end': 5,
'emptyBlocksIndex': 1
});
var innerElement = window.getSelection().getRangeAt(0).startContainer;
// The behavior varies from browser to browser for this case, some select TH, some #textNode
expect(Util.isDescendant(editor.elements[0].querySelector('th'), innerElement, true))
.toBe(true, 'expect selection to be of TH or a descendant');
expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);
});
it('should not import a selection beyond any block elements that have text, even when emptyBlocksIndex indicates it should ', function () {
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2>Not Empty</h2><p><b><i>Whatever</i></b></p>';
var editor = this.newMediumEditor('.editor', {
buttons: ['italic', 'underline', 'strikethrough']
});
// Import a selection that indicates the text should be at the end of the 'www.google.com' word, but in the 3rd paragraph (at the beginning of 'Whatever')
editor.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 3
});
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(Util.isDescendant(editor.elements[0].querySelectorAll('p')[1], innerElement, true)).toBe(false, 'moved selection beyond non-empty block element');
expect(Util.isDescendant(editor.elements[0].querySelector('h2'), innerElement, true)).toBe(true, 'moved selection to element to incorrect block element');
});
});

@@ -218,3 +444,3 @@

expect(elements.length).toBe(1);
expect(elements[0].tagName.toLowerCase()).toBe('i');
expect(elements[0].nodeName.toLowerCase()).toBe('i');
expect(elements[0].innerHTML).toBe('ipsum');

@@ -231,3 +457,3 @@ });

expect(elements.length).toBe(1);
expect(elements[0].tagName.toLowerCase()).toBe('i');
expect(elements[0].nodeName.toLowerCase()).toBe('i');
expect(elements[0].innerHTML).toBe('ipsum');

@@ -234,0 +460,0 @@ });

@@ -29,3 +29,3 @@ /*global describe, it, beforeEach, afterEach, expect,

textarea = this.el;
expect(editor.elements[0].tagName.toLowerCase()).toBe('div');
expect(editor.elements[0].nodeName.toLowerCase()).toBe('div');

@@ -32,0 +32,0 @@ var attributesToPreserve = ['data-disable-editing',

/*global MediumEditor, Util, describe, it, expect, spyOn,
afterEach, beforeEach, setupTestHelpers */
afterEach, beforeEach, setupTestHelpers, selectElementContents */

@@ -262,3 +262,3 @@ describe('Util', function () {

expect(closestTag.tagName.toLowerCase()).toBe('span');
expect(closestTag.nodeName.toLowerCase()).toBe('span');
});

@@ -358,2 +358,28 @@

});
describe('execFormatBlock', function () {
it('should execute indent command when called with blockquote when selection is inside a nested block element within a blockquote', function () {
var el = this.createElement('div', '', '<blockquote><p>Some <b>Text</b></p></blockquote>');
el.setAttribute('contenteditable', true);
selectElementContents(el.querySelector('b'));
spyOn(document, 'execCommand');
Util.execFormatBlock(document, 'blockquote');
expect(document.execCommand).toHaveBeenCalledWith('outdent', false, null);
});
it('should execute indent command when called with blockquote when isIE is true', function () {
var origIsIE = Util.isIE,
el = this.createElement('div', '', '<p>Some <b>Text</b></p>');
Util.isIE = true;
el.setAttribute('contenteditable', true);
selectElementContents(el.querySelector('b'));
spyOn(document, 'execCommand');
Util.execFormatBlock(document, 'blockquote');
expect(document.execCommand).toHaveBeenCalledWith('indent', false, 'blockquote');
Util.isIE = origIsIE;
});
});
});

@@ -31,3 +31,3 @@ /*global Util, Selection, Extension,

var node = Selection.getSelectionStart(this.options.ownerDocument),
tag = node && node.tagName.toLowerCase();
tag = node && node.nodeName.toLowerCase();

@@ -54,3 +54,3 @@ if (tag === 'pre') {

var p, node = Selection.getSelectionStart(this.options.ownerDocument),
tagName = node.tagName.toLowerCase(),
tagName = node.nodeName.toLowerCase(),
isEmpty = /^(\s+|<br\/?>)?$/i,

@@ -89,3 +89,3 @@ isHeader = /h\d/i;

// when the next tag *is* a header
isHeader.test(node.nextElementSibling.tagName)) {
isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {
// hitting delete in an empty element preceding a header, ex:

@@ -112,3 +112,3 @@ // <p>[CURSOR]</p><h1>Header</h1>

node.nextElementSibling &&
node.nextElementSibling.tagName.toLowerCase() === 'li') {
node.nextElementSibling.nodeName.toLowerCase() === 'li') {
// backspacing in an empty first list element in the first list (with more elements) ex:

@@ -145,3 +145,3 @@ // <ul><li>[CURSOR]</li><li>List Item 2</li></ul>

if (node.getAttribute('data-medium-editor-element') && node.children.length === 0) {
if (Util.isMediumEditorElement(node) && node.children.length === 0) {
this.options.ownerDocument.execCommand('formatBlock', false, 'p');

@@ -151,3 +151,3 @@ }

if (Util.isKey(event, Util.keyCode.ENTER) && !Util.isListItem(node)) {
tagName = node.tagName.toLowerCase();
tagName = node.nodeName.toLowerCase();
// For anchor tags, unlink

@@ -216,3 +216,3 @@ if (tagName === 'a') {

elements.forEach(function (element) {
if (element.tagName.toLowerCase() === 'textarea') {
if (element.nodeName.toLowerCase() === 'textarea') {
this.elements.push(createContentEditable.call(this, element));

@@ -611,4 +611,5 @@ } else {

*/
addBuiltInExtension: function (name) {
var extension = this.getExtensionByName(name);
addBuiltInExtension: function (name, opts) {
var extension = this.getExtensionByName(name),
merged;
if (extension) {

@@ -620,3 +621,4 @@ return extension;

case 'anchor':
extension = new MediumEditor.extensions.anchor(this.options.anchor);
merged = Util.extend({}, this.options.anchor, opts);
extension = new MediumEditor.extensions.anchor(merged);
break;

@@ -630,3 +632,3 @@ case 'anchorPreview':

case 'fontsize':
extension = new MediumEditor.extensions.fontSize();
extension = new MediumEditor.extensions.fontSize(opts);
break;

@@ -649,3 +651,8 @@ case 'imageDragging':

if (MediumEditor.extensions.button.isBuiltInButton(name)) {
extension = new MediumEditor.extensions.button(name);
if (opts) {
merged = Util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);
extension = new MediumEditor.extensions.button(merged);
} else {
extension = new MediumEditor.extensions.button(name);
}
}

@@ -670,3 +677,2 @@ break;

// NOT DOCUMENTED - exposed as extension helper and for backwards compatability
checkSelection: function () {

@@ -811,2 +817,13 @@ var toolbar = this.getExtensionByName('toolbar');

};
// If start = 0 there may still be an empty paragraph before it, but we don't care.
if (start !== 0) {
var emptyBlocksIndex = Selection.getIndexRelativeToAdjacentEmptyBlocks(
this.options.ownerDocument,
this.elements[editableElementIndex],
range.startContainer,
range.startOffset);
if (emptyBlocksIndex !== 0) {
selectionState.emptyBlocksIndex = emptyBlocksIndex;
}
}
}

@@ -886,2 +903,21 @@ }

if (inSelectionState.emptyBlocksIndex && selectionState.end === nextCharIndex) {
var targetNode = Util.getTopBlockContainer(range.startContainer),
index = 0;
// Skip over empty blocks until we hit the block we want the selection to be in
while (index < inSelectionState.emptyBlocksIndex && targetNode.nextSibling) {
targetNode = targetNode.nextSibling;
index++;
// If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
if (targetNode.textContent.length > 0) {
break;
}
}
// We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
// element at the beginning of the block
range.setStart(Util.getFirstSelectableLeafNode(targetNode), 0);
range.collapse(true);
}
// If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.

@@ -891,3 +927,2 @@ if (favorLaterSelectionAnchor) {

}
sel = this.options.contentWindow.getSelection();

@@ -894,0 +929,0 @@ sel.removeAllRanges();

@@ -88,10 +88,2 @@ var buttonDefaults;

},
'quote': {
name: 'quote',
action: 'append-blockquote',
aria: 'blockquote',
tagNames: ['blockquote'],
contentDefault: '<b>&ldquo;</b>',
contentFA: '<i class="fa fa-quote-right"></i>'
},
'orderedlist': {

@@ -115,10 +107,2 @@ name: 'orderedlist',

},
'pre': {
name: 'pre',
action: 'append-pre',
aria: 'preformatted text',
tagNames: ['pre'],
contentDefault: '<b>0101</b>',
contentFA: '<i class="fa fa-code fa-lg"></i>'
},
'indent': {

@@ -188,28 +172,2 @@ name: 'indent',

},
'header1': {
name: 'header1',
action: function (options) {
return 'append-' + options.firstHeader;
},
aria: function (options) {
return options.firstHeader;
},
tagNames: function (options) {
return [options.firstHeader];
},
contentDefault: '<b>H1</b>'
},
'header2': {
name: 'header2',
action: function (options) {
return 'append-' + options.secondHeader;
},
aria: function (options) {
return options.secondHeader;
},
tagNames: function (options) {
return [options.secondHeader];
},
contentDefault: '<b>H2</b>'
},
// Known inline elements that are not removed, or not removed consistantly across browsers:

@@ -223,2 +181,69 @@ // <span>, <label>, <br>

contentFA: '<i class="fa fa-eraser"></i>'
},
/***** Buttons for appending block elements (append-<element> action) *****/
'quote': {
name: 'quote',
action: 'append-blockquote',
aria: 'blockquote',
tagNames: ['blockquote'],
contentDefault: '<b>&ldquo;</b>',
contentFA: '<i class="fa fa-quote-right"></i>'
},
'pre': {
name: 'pre',
action: 'append-pre',
aria: 'preformatted text',
tagNames: ['pre'],
contentDefault: '<b>0101</b>',
contentFA: '<i class="fa fa-code fa-lg"></i>'
},
'h1': {
name: 'h1',
action: 'append-h1',
aria: 'header type one',
tagNames: ['h1'],
contentDefault: '<b>H1</b>',
contentFA: '<i class="fa fa-header"><sup>1</sup>'
},
'h2': {
name: 'h2',
action: 'append-h2',
aria: 'header type two',
tagNames: ['h2'],
contentDefault: '<b>H2</b>',
contentFA: '<i class="fa fa-header"><sup>2</sup>'
},
'h3': {
name: 'h3',
action: 'append-h3',
aria: 'header type three',
tagNames: ['h3'],
contentDefault: '<b>H3</b>',
contentFA: '<i class="fa fa-header"><sup>3</sup>'
},
'h4': {
name: 'h4',
action: 'append-h4',
aria: 'header type four',
tagNames: ['h4'],
contentDefault: '<b>H4</b>',
contentFA: '<i class="fa fa-header"><sup>4</sup>'
},
'h5': {
name: 'h5',
action: 'append-h5',
aria: 'header type five',
tagNames: ['h5'],
contentDefault: '<b>H5</b>',
contentFA: '<i class="fa fa-header"><sup>5</sup>'
},
'h6': {
name: 'h6',
action: 'append-h6',
aria: 'header type six',
tagNames: ['h6'],
contentDefault: '<b>H6</b>',
contentFA: '<i class="fa fa-header"><sup>6</sup>'
}

@@ -225,0 +250,0 @@ };

@@ -17,4 +17,2 @@ var editorDefaults;

ownerDocument: document,
firstHeader: 'h3',
secondHeader: 'h4',
targetBlank: false,

@@ -21,0 +19,0 @@ extensions: {},

@@ -63,5 +63,6 @@ var AnchorForm;

var selectedParentElement = Selection.getSelectedParentElement(Selection.getSelectionRange(this.document));
if (selectedParentElement.tagName &&
selectedParentElement.tagName.toLowerCase() === 'a') {
var selectedParentElement = Selection.getSelectedParentElement(Selection.getSelectionRange(this.document)),
firstTextNode = Util.getFirstTextNode(selectedParentElement);
if (Util.getClosestTag(firstTextNode, 'a')) {
return this.execAction('unlink');

@@ -68,0 +69,0 @@ }

/*global Extension, Util */
var AutoLink,
WHITESPACE_CHARS,
KNOWN_TLDS_FRAGMENT,
LINK_REGEXP_TEXT;
WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
'\u2028', '\u2029'];
KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +

@@ -130,8 +133,12 @@ 'xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|' +

documentModified = true;
// Some editing has happened to the span, so just remove it entirely. The user can put it back
// around just the href content if they need to prevent it from linking
while (spans[i].childNodes.length > 0) {
spans[i].parentNode.insertBefore(spans[i].firstChild, spans[i]);
var trimmedTextContent = textContent.replace(/\s+$/, '');
if (spans[i].getAttribute('data-href') === trimmedTextContent) {
var charactersTrimmed = textContent.length - trimmedTextContent.length,
subtree = Util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));
spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);
} else {
// Some editing has happened to the span, so just remove it entirely. The user can put it back
// around just the href content if they need to prevent it from linking
Util.unwrap(spans[i], this.document);
}
spans[i].parentNode.removeChild(spans[i]);
}

@@ -142,2 +149,28 @@ }

splitTextBeforeEnd: function (element, characterCount) {
var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
lastChildNotExhausted = true;
// Start the tree walker at the last descendant of the span
while (lastChildNotExhausted) {
lastChildNotExhausted = treeWalker.lastChild() !== null;
}
var currentNode,
currentNodeValue,
previousNode;
while (characterCount > 0 && previousNode !== null) {
currentNode = treeWalker.currentNode;
currentNodeValue = currentNode.nodeValue;
if (currentNodeValue.length > characterCount) {
previousNode = currentNode.splitText(currentNodeValue.length - characterCount);
characterCount = 0;
} else {
previousNode = treeWalker.previousNode();
characterCount -= currentNodeValue.length;
}
}
return previousNode;
},
performLinkingWithinElement: function (element) {

@@ -156,4 +189,2 @@ var matches = this.findLinkableText(element),

var linkRegExp = new RegExp(LINK_REGEXP_TEXT, 'gi'),
whitespaceChars = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
'\u2028', '\u2029'],
textContent = contenteditable.textContent,

@@ -167,4 +198,4 @@ match = null,

// If the regexp detected something as a link that has text immediately preceding/following it, bail out.
matchOk = (match.index === 0 || whitespaceChars.indexOf(textContent[match.index - 1]) !== -1) &&
(matchEnd === textContent.length || whitespaceChars.indexOf(textContent[matchEnd]) !== -1);
matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&
(matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);
// If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.

@@ -171,0 +202,0 @@ matchOk = matchOk && (match[0].indexOf('/') !== -1 ||

@@ -81,2 +81,12 @@ var Button;

/* classList: [Array]
* An array of classNames (strings) to be added to the button
*/
classList: undefined,
/* attrs: [object]
* A set of key-value pairs to add to the button as custom attributes
*/
attrs: undefined,
/* buttonDefaults: [Object]

@@ -134,4 +144,12 @@ * Set of default config options for all of the built-in MediumEditor buttons

buttonLabels = this.getEditorOption('buttonLabels');
// Add class names
button.classList.add('medium-editor-action');
button.classList.add('medium-editor-action-' + this.name);
if (this.classList) {
this.classList.forEach(function (className) {
button.classList.add(className);
});
}
// Add attributes
button.setAttribute('data-action', this.getAction());

@@ -142,9 +160,11 @@ if (ariaLabel) {

}
if (buttonLabels) {
if (buttonLabels === 'fontawesome' && this.contentFA) {
content = this.contentFA;
} else if (typeof buttonLabels === 'object' && buttonLabels[this.name]) {
content = buttonLabels[this.name];
}
if (this.attrs) {
Object.keys(this.attrs).forEach(function (attr) {
button.setAttribute(attr, this.attrs[attr]);
}, this);
}
if (buttonLabels === 'fontawesome' && this.contentFA) {
content = this.contentFA;
}
button.innerHTML = content;

@@ -197,4 +217,4 @@ return button;

if (tagNames && tagNames.length > 0 && node.tagName) {
isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
if (tagNames && tagNames.length > 0) {
isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;
}

@@ -201,0 +221,0 @@

@@ -146,3 +146,3 @@ var FontSizeForm;

Selection.getSelectedElements(this.document).forEach(function (el) {
if (el.tagName === 'FONT' && el.hasAttribute('size')) {
if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {
el.removeAttribute('size');

@@ -149,0 +149,0 @@ }

@@ -23,3 +23,3 @@ var KeyboardCommands;

command: 'bold',
key: 'b',
key: 'B',
meta: true,

@@ -30,3 +30,3 @@ shift: false

command: 'italic',
key: 'i',
key: 'I',
meta: true,

@@ -37,3 +37,3 @@ shift: false

command: 'underline',
key: 'u',
key: 'U',
meta: true,

@@ -66,2 +66,3 @@ shift: false

isShift = !!event.shiftKey;
this.keys[keyCode].forEach(function (data) {

@@ -72,3 +73,7 @@ if (data.meta === isMeta &&

event.stopPropagation();
this.execAction(data.command);
// command can be false so the shortcurt is just disabled
if (false !== data.command) {
this.execAction(data.command);
}
}

@@ -75,0 +80,0 @@ }, this);

@@ -173,3 +173,3 @@ /*global Util, Selection, Extension */

switch (workEl.tagName.toLowerCase()) {
switch (workEl.nodeName.toLowerCase()) {
case 'p':

@@ -206,3 +206,3 @@ case 'div':

if ('a' === workEl.tagName.toLowerCase() && this.getEditorOption('targetBlank')) {
if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {
Util.setTargetBlank(workEl);

@@ -219,3 +219,3 @@ }

isCommonBlock: function (el) {
return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
},

@@ -222,0 +222,0 @@

@@ -21,3 +21,3 @@ var Toolbar;

*/
buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],

@@ -120,3 +120,5 @@ /* diffLeft: [Number]

buttons,
extension;
extension,
buttonName,
buttonOpts;

@@ -128,9 +130,14 @@ ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();

this.buttons.forEach(function (button) {
extension = this.base.getExtensionByName(button);
if (!extension) {
// If button hasn't been passed as an extension, create it
extension = this.base.addBuiltInExtension(button);
if (typeof button === 'string') {
buttonName = button;
buttonOpts = null;
} else {
buttonName = button.name;
buttonOpts = button;
}
// If the button already exists as an extension, it'll be returned
// othwerise it'll create the default built-in button
extension = this.base.addBuiltInExtension(buttonName, buttonOpts);
if (extension && typeof extension.getButton === 'function') {

@@ -448,4 +455,2 @@ btn = extension.getButton(this.base);

parentNode = Selection.getSelectedParentElement(selectionRange);
// Loop through all extensions

@@ -469,8 +474,17 @@ this.forEachExtension(function (extension) {

parentNode = Selection.getSelectedParentElement(selectionRange);
// Make sure the selection parent isn't outside of the contenteditable
if (!this.getEditorElements().some(function (element) {
return Util.isDescendant(element, parentNode, true);
})) {
return;
}
// Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node
while (parentNode.tagName !== undefined && Util.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
while (parentNode) {
manualStateChecks.forEach(updateExtensionState);
// we can abort the search upwards if we leave the contentEditable element
if (this.getEditorElements().indexOf(parentNode) !== -1) {
if (Util.isMediumEditorElement(parentNode)) {
break;

@@ -477,0 +491,0 @@ }

@@ -7,2 +7,10 @@ /*global Util */

function filterOnlyParentElements(node) {
if (Util.isBlockContainer(node)) {
return NodeFilter.FILTER_ACCEPT;
} else {
return NodeFilter.FILTER_SKIP;
}
}
Selection = {

@@ -26,3 +34,3 @@ findMatchingSelectionParent: function (testElementFunction, contentWindow) {

return this.findMatchingSelectionParent(function (el) {
return el.getAttribute('data-medium-editor-element');
return Util.isMediumEditorElement(el);
}, contentWindow);

@@ -64,2 +72,41 @@ },

// Returns 0 unless the cursor is within or preceded by empty paragraphs/blocks,
// in which case it returns the count of such preceding paragraphs, including
// the empty paragraph in which the cursor itself may be embedded.
getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) {
// If there is text in front of the cursor, that means there isn't only empty blocks before it
if (cursorContainer.nodeType === 3 && cursorOffset > 0) {
return 0;
}
// Check if the block that contains the cursor has any other text in front of the cursor
var node = cursorContainer;
if (node.nodeType !== 3) {
//node = cursorContainer.childNodes.length === cursorOffset ? null : cursorContainer.childNodes[cursorOffset];
node = cursorContainer.childNodes[cursorOffset];
}
if (node && !Util.isElementAtBeginningOfBlock(node)) {
return 0;
}
// Walk over block elements, counting number of empty blocks between last piece of text
// and the block the cursor is in
var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
emptyBlocksCount = 0;
while (treeWalker.nextNode()) {
var blockIsEmpty = treeWalker.currentNode.textContent === '';
if (blockIsEmpty || emptyBlocksCount > 0) {
emptyBlocksCount += 1;
}
if (Util.isDescendant(treeWalker.currentNode, cursorContainer, true)) {
return emptyBlocksCount;
}
if (!blockIsEmpty) {
emptyBlocksCount = 0;
}
}
return emptyBlocksCount;
},
selectionInContentEditableFalse: function (contentWindow) {

@@ -228,24 +275,4 @@ // determine if the current selection is exclusively inside

return startNode;
},
getSelectionData: function (el) {
var tagName;
if (el && el.tagName) {
tagName = el.tagName.toLowerCase();
}
while (el && Util.parentElements.indexOf(tagName) === -1) {
el = el.parentNode;
if (el && el.tagName) {
tagName = el.tagName.toLowerCase();
}
}
return {
el: el,
tagName: tagName
};
}
};
}());

@@ -1,2 +0,2 @@

/*global NodeFilter, console, Selection*/
/*global NodeFilter, Selection*/

@@ -44,3 +44,3 @@ var Util;

DELETE: 46,
K: 107
K: 75 // K keycode, and not k
},

@@ -92,3 +92,4 @@

parentElements: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
blockContainerElementNames: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'],
emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],

@@ -156,4 +157,2 @@ extend: function extend(/* dest, source1, source2, ...*/) {

now: Date.now,
// https://github.com/jashkenas/underscore

@@ -214,3 +213,3 @@ throttle: function (func, wait) {

// do not traverse upwards past the nearest containing editor
if (current.getAttribute('data-medium-editor-element')) {
if (Util.isMediumEditorElement(current)) {
return false;

@@ -252,3 +251,3 @@ }

toReplace.parentNode.childNodes.length === 1 &&
!toReplace.parentNode.getAttribute('data-medium-editor-element')) {
!Util.isMediumEditorElement(toReplace.parentNode)) {
toReplace = toReplace.parentNode;

@@ -281,21 +280,36 @@ }

execFormatBlock: function (doc, tagName) {
var selectionData = Selection.getSelectionData(Selection.getSelectionStart(doc));
// FF handles blockquote differently on formatBlock
// allowing nesting, we need to use outdent
// https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
if (tagName === 'blockquote' && selectionData.el &&
selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
return doc.execCommand('outdent', false, null);
// Get the top level block element that contains the selection
var blockContainer = Util.getTopBlockContainer(Selection.getSelectionStart(doc));
// Special handling for blockquote
if (tagName === 'blockquote') {
if (blockContainer) {
var childNodes = Array.prototype.slice.call(blockContainer.childNodes);
// Check if the blockquote has a block element as a child (nested blocks)
if (childNodes.some(function (childNode) {
return Util.isBlockContainer(childNode);
})) {
// FF handles blockquote differently on formatBlock
// allowing nesting, we need to use outdent
// https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
return doc.execCommand('outdent', false, null);
}
}
// When IE blockquote needs to be called as indent
// http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
if (this.isIE) {
return doc.execCommand('indent', false, tagName);
}
}
if (selectionData.tagName === tagName) {
// If the blockContainer is already the element type being passed in
// treat it as 'undo' formatting and just convert it to a <p>
if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {
tagName = 'p';
}
// When IE we need to add <> to heading elements and
// blockquote needs to be called as indent
// When IE we need to add <> to heading elements
// http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
// http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
if (this.isIE) {
if (tagName === 'blockquote') {
return doc.execCommand('indent', false, tagName);
}
tagName = '<' + tagName + '>';

@@ -318,3 +332,3 @@ }

var i, url = anchorUrl || false;
if (el.tagName.toLowerCase() === 'a') {
if (el.nodeName.toLowerCase() === 'a') {
el.target = '_blank';

@@ -336,3 +350,3 @@ } else {

j;
if (el.tagName.toLowerCase() === 'a') {
if (el.nodeName.toLowerCase() === 'a') {
for (j = 0; j < classes.length; j += 1) {

@@ -355,3 +369,3 @@ el.classList.add(classes[j]);

}
if (node.tagName.toLowerCase() === 'li') {
if (node.nodeName.toLowerCase() === 'li') {
return true;

@@ -361,4 +375,4 @@ }

var parentNode = node.parentNode,
tagName = parentNode.tagName.toLowerCase();
while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
tagName = parentNode.nodeName.toLowerCase();
while (!this.isBlockContainer(parentNode) && tagName !== 'div') {
if (tagName === 'li') {

@@ -368,4 +382,4 @@ return true;

parentNode = parentNode.parentNode;
if (parentNode && parentNode.tagName) {
tagName = parentNode.tagName.toLowerCase();
if (parentNode) {
tagName = parentNode.nodeName.toLowerCase();
} else {

@@ -379,3 +393,3 @@ return false;

cleanListDOM: function (ownerDocument, element) {
if (element.tagName.toLowerCase() !== 'li') {
if (element.nodeName.toLowerCase() !== 'li') {
return;

@@ -386,3 +400,3 @@ }

if (list.parentElement.tagName.toLowerCase() === 'p') { // yes we need to clean up
if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up
this.unwrap(list.parentElement, ownerDocument);

@@ -613,2 +627,69 @@

isElementAtBeginningOfBlock: function (node) {
var textVal,
sibling;
while (!this.isBlockContainer(node) && !this.isMediumEditorElement(node)) {
sibling = node;
while (sibling = sibling.previousSibling) {
textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;
if (textVal.length > 0) {
return false;
}
}
node = node.parentNode;
}
return true;
},
isMediumEditorElement: function (element) {
return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');
},
isBlockContainer: function (element) {
return element && element.nodeType !== 3 && this.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;
},
getTopBlockContainer: function (element) {
var topBlock = element;
this.traverseUp(element, function (el) {
if (Util.isBlockContainer(el)) {
topBlock = el;
}
return false;
});
return topBlock;
},
getFirstSelectableLeafNode: function (element) {
while (element && element.firstChild) {
element = element.firstChild;
}
// We don't want to set the selection to an element that can't have children, this messes up Gecko.
element = this.traverseUp(element, function (el) {
return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;
});
// Selecting at the beginning of a table doesn't work in PhantomJS.
if (element.nodeName.toLowerCase() === 'table') {
var firstCell = element.querySelector('th, td');
if (firstCell) {
element = firstCell;
}
}
return element;
},
getFirstTextNode: function (element) {
if (element.nodeType === 3) {
return element;
}
for (var i = 0; i < element.childNodes.length; i++) {
var textNode = this.getFirstTextNode(element.childNodes[i]);
if (textNode !== null) {
return textNode;
}
}
return null;
},
ensureUrlHasProtocol: function (url) {

@@ -623,3 +704,3 @@ if (url.indexOf('://') === -1) {

if (window.console !== undefined && typeof window.console.warn === 'function') {
window.console.warn.apply(console, arguments);
window.console.warn.apply(window.console, arguments);
}

@@ -653,3 +734,3 @@ },

tags.forEach(function (tag) {
if (el.tagName.toLowerCase() === tag) {
if (el.nodeName.toLowerCase() === tag) {
el.parentNode.removeChild(el);

@@ -663,3 +744,3 @@ }

return this.traverseUp(el, function (element) {
return element.tagName.toLowerCase() === tag.toLowerCase();
return element.nodeName.toLowerCase() === tag.toLowerCase();
});

@@ -666,0 +747,0 @@ },

@@ -20,3 +20,3 @@ /*global MediumEditor */

// grunt-bump looks for this:
'version': '5.0.0-alpha.0'
'version': '5.0.0-rc.1'
}).version);

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc