Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create Embed buttons to trigger the embed functionality
Fixes #233
- Loading branch information
Showing
3 changed files
with
337 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
src/ui/react/src/components/buttons/button-embed-edit.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
(function () { | ||
'use strict'; | ||
|
||
var KEY_ENTER = 13; | ||
var KEY_ESC = 27; | ||
|
||
/** | ||
* The ButtonEmbedEdit class provides functionality for creating and editing an embed link in a document. | ||
* Provides UI for creating and editing an embed link. | ||
* | ||
* @class ButtonEmbedEdit | ||
*/ | ||
var ButtonEmbedEdit = React.createClass({ | ||
// Allows validating props being passed to the component. | ||
propTypes: { | ||
/** | ||
* The editor instance where the component is being used. | ||
* | ||
* @property {Object} editor | ||
*/ | ||
editor: React.PropTypes.object.isRequired | ||
}, | ||
|
||
// Lifecycle. Provides static properties to the widget. | ||
statics: { | ||
/** | ||
* The name which will be used as an alias of the button in the configuration. | ||
* | ||
* @static | ||
* @property {String} key | ||
* @default embedEdit | ||
*/ | ||
key: 'embedEdit' | ||
}, | ||
|
||
/** | ||
* Lifecycle. Invoked once, only on the client, immediately after the initial rendering occurs. | ||
* | ||
* Focuses on the link input to immediately allow editing. This should only happen if the component | ||
* is rendered in exclusive mode to prevent aggressive focus stealing. | ||
* | ||
* @method componentDidMount | ||
*/ | ||
componentDidMount: function () { | ||
if (this.props.renderExclusive || this.props.manualSelection) { | ||
// We need to wait for the next rendering cycle before focusing to avoid undesired | ||
// scrolls on the page | ||
if (window.requestAnimationFrame) { | ||
window.requestAnimationFrame(this._focusLinkInput); | ||
} else { | ||
setTimeout(this._focusLinkInput, 0); | ||
} | ||
} | ||
}, | ||
|
||
/** | ||
* Lifecycle. Invoked when a component is receiving new props. | ||
* This method is not called for the initial render. | ||
* | ||
* @method componentWillReceiveProps | ||
*/ | ||
componentWillReceiveProps: function(nextProps) { | ||
this.replaceState(this.getInitialState()); | ||
}, | ||
|
||
/** | ||
* Lifecycle. Invoked once before the component is mounted. | ||
* The return value will be used as the initial value of this.state. | ||
* | ||
* @method getInitialState | ||
*/ | ||
getInitialState: function() { | ||
var editor = this.props.editor.get('nativeEditor'); | ||
var embed; | ||
|
||
var selection = editor.getSelection(); | ||
|
||
if (selection) { | ||
var range = selection.getRanges()[0]; | ||
|
||
if (range) { | ||
range.shrink(CKEDITOR.SHRINK_TEXT); | ||
|
||
embed = editor.elementPath(range.getCommonAncestor()).contains(function(element) { | ||
return element.getAttribute('data-widget') === 'ae_embed' || | ||
(element.getAttribute('data-cke-widget-wrapper') && element.find('[data-widget="ae_embed"]')); | ||
}, 1); | ||
|
||
if (embed && embed.getAttribute('data-widget') !== 'ae_embed') { | ||
embed = embed.find('[data-widget="ae_embed"]').getItem(0); | ||
} | ||
} | ||
} | ||
|
||
var href = embed ? embed.getAttribute('data-ae-embed-url') : ''; | ||
|
||
return { | ||
initialLink: { | ||
href: href | ||
}, | ||
linkHref: href | ||
}; | ||
}, | ||
|
||
/** | ||
* Lifecycle. Renders the UI of the button. | ||
* | ||
* @method render | ||
* @return {Object} The content which should be rendered. | ||
*/ | ||
render: function() { | ||
var clearLinkStyle = { | ||
opacity: this.state.linkHref ? 1 : 0 | ||
}; | ||
|
||
return ( | ||
<div className="ae-container-edit-link"> | ||
<div className="ae-container-input xxl"> | ||
<input className="ae-input" onChange={this._handleLinkHrefChange} onKeyDown={this._handleKeyDown} placeholder={AlloyEditor.Strings.editLink} ref="linkInput" type="text" value={this.state.linkHref}></input> | ||
<button aria-label={AlloyEditor.Strings.clearInput} className="ae-button ae-icon-remove" onClick={this._clearLink} style={clearLinkStyle} title={AlloyEditor.Strings.clear}></button> | ||
</div> | ||
<button aria-label={AlloyEditor.Strings.confirm} className="ae-button" disabled={!this._isValidState()} onClick={this._embedLink} title={AlloyEditor.Strings.confirm}> | ||
<span className="ae-icon-ok"></span> | ||
</button> | ||
</div> | ||
); | ||
}, | ||
|
||
/** | ||
* Clears the link input. This only changes the component internal state, but does not | ||
* affect the link element of the editor. Only the _removeLink and _updateLink methods | ||
* are translated to the editor element. | ||
* | ||
* @protected | ||
* @method _clearLink | ||
*/ | ||
_clearLink: function() { | ||
this.setState({ | ||
linkHref: '' | ||
}); | ||
}, | ||
|
||
/** | ||
* Triggers the embedUrl command to transform the link into an embed media object | ||
* | ||
* @protected | ||
* @method _embedLink | ||
*/ | ||
_embedLink: function() { | ||
var nativeEditor = this.props.editor.get('nativeEditor'); | ||
|
||
nativeEditor.execCommand('embedUrl', { | ||
url: this.state.linkHref | ||
}); | ||
|
||
// We need to cancelExclusive with the bound parameters in case the button is used | ||
// inside another in exclusive mode (such is the case of the link button) | ||
this.props.cancelExclusive(); | ||
}, | ||
|
||
/** | ||
* Focuses the user cursor on the widget's input. | ||
* | ||
* @protected | ||
* @method _focusLinkInput | ||
*/ | ||
_focusLinkInput: function() { | ||
ReactDOM.findDOMNode(this.refs.linkInput).focus(); | ||
}, | ||
|
||
/** | ||
* Monitors key interaction inside the input element to respond to the keys: | ||
* - Enter: Creates/updates the link. | ||
* - Escape: Discards the changes. | ||
* | ||
* @protected | ||
* @method _handleKeyDown | ||
* @param {SyntheticEvent} event The keyboard event. | ||
*/ | ||
_handleKeyDown: function(event) { | ||
if (event.keyCode === KEY_ENTER || event.keyCode === KEY_ESC) { | ||
event.preventDefault(); | ||
} | ||
|
||
if (event.keyCode === KEY_ENTER) { | ||
this._embedLink(); | ||
} else if (event.keyCode === KEY_ESC) { | ||
var editor = this.props.editor.get('nativeEditor'); | ||
|
||
// We need to cancelExclusive with the bound parameters in case the button is used | ||
// inside another in exclusive mode (such is the case of the link button) | ||
this.props.cancelExclusive(); | ||
|
||
editor.fire('actionPerformed', this); | ||
} | ||
}, | ||
|
||
/** | ||
* Updates the component state when the link input changes on user interaction. | ||
* | ||
* @protected | ||
* @method _handleLinkHrefChange | ||
* @param {SyntheticEvent} event The change event. | ||
*/ | ||
_handleLinkHrefChange: function(event) { | ||
this.setState({ | ||
linkHref: event.target.value | ||
}); | ||
}, | ||
|
||
/** | ||
* Verifies that the current link state is valid so the user can save the link. A valid state | ||
* means that we have a non-empty href that's different from the original one. | ||
* | ||
* @protected | ||
* @method _isValidState | ||
* @return {Boolean} [description] | ||
*/ | ||
_isValidState: function() { | ||
var validState = | ||
this.state.linkHref && ( | ||
this.state.linkHref !== this.state.initialLink.href | ||
); | ||
|
||
return validState; | ||
} | ||
}); | ||
|
||
AlloyEditor.Buttons[ButtonEmbedEdit.key] = AlloyEditor.ButtonEmbedEdit = ButtonEmbedEdit; | ||
}()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
(function () { | ||
'use strict'; | ||
|
||
/** | ||
* The ButtonEmbed class provides functionality for creating and editing an embed link in a document. | ||
* ButtonEmbed renders in two different modes: | ||
* | ||
* - Normal: Just a button that allows to switch to the edition mode | ||
* - Exclusive: The ButtonEmbedEdit UI with all the link edition controls. | ||
* | ||
* @uses ButtonKeystroke | ||
* | ||
* @class ButtonEmbed | ||
*/ | ||
var ButtonEmbed = React.createClass({ | ||
mixins: [AlloyEditor.ButtonKeystroke], | ||
|
||
// Allows validating props being passed to the component. | ||
propTypes: { | ||
/** | ||
* The editor instance where the component is being used. | ||
* | ||
* @property {Object} editor | ||
*/ | ||
editor: React.PropTypes.object.isRequired, | ||
|
||
/** | ||
* The label that should be used for accessibility purposes. | ||
* | ||
* @property {String} label | ||
*/ | ||
label: React.PropTypes.string, | ||
|
||
/** | ||
* The tabIndex of the button in its toolbar current state. A value other than -1 | ||
* means that the button has focus and is the active element. | ||
* | ||
* @property {Number} tabIndex | ||
*/ | ||
tabIndex: React.PropTypes.number | ||
}, | ||
|
||
// Lifecycle. Provides static properties to the widget. | ||
statics: { | ||
/** | ||
* The name which will be used as an alias of the button in the configuration. | ||
* | ||
* @static | ||
* @property {String} key | ||
* @default embed | ||
*/ | ||
key: 'embed' | ||
}, | ||
|
||
/** | ||
* Lifecycle. Returns the default values of the properties used in the widget. | ||
* | ||
* @method getDefaultProps | ||
* @return {Object} The default properties. | ||
*/ | ||
getDefaultProps: function() { | ||
return { | ||
keystroke: { | ||
fn: '_requestExclusive', | ||
keys: CKEDITOR.CTRL + CKEDITOR.SHIFT + 76 /*L*/ | ||
} | ||
}; | ||
}, | ||
|
||
/** | ||
* Lifecycle. Renders the UI of the button. | ||
* | ||
* @method render | ||
* @return {Object} The content which should be rendered. | ||
*/ | ||
render: function() { | ||
if (this.props.renderExclusive) { | ||
return ( | ||
<AlloyEditor.ButtonEmbedEdit {...this.props} /> | ||
); | ||
} else { | ||
return ( | ||
<button aria-label={AlloyEditor.Strings.link} className="ae-button" data-type="button-embed" onClick={this._requestExclusive} tabIndex={this.props.tabIndex} title={AlloyEditor.Strings.link}> | ||
<span className="ae-icon-add"></span> | ||
</button> | ||
); | ||
} | ||
}, | ||
|
||
/** | ||
* Requests the link button to be rendered in exclusive mode to allow the embedding of a link. | ||
* | ||
* @protected | ||
* @method _requestExclusive | ||
*/ | ||
_requestExclusive: function() { | ||
this.props.requestExclusive(ButtonEmbed.key); | ||
} | ||
}); | ||
|
||
AlloyEditor.Buttons[ButtonEmbed.key] = AlloyEditor.ButtonEmbed = ButtonEmbed; | ||
}()); |