Skip to content

Commit

Permalink
Create Embed buttons to trigger the embed functionality
Browse files Browse the repository at this point in the history
Fixes #233
  • Loading branch information
jbalsas authored and ipeychev committed Feb 18, 2016
1 parent 0544f79 commit 6fc3959
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 2 deletions.
7 changes: 5 additions & 2 deletions src/ui/react/src/adapter/alloy-editor.js
Expand Up @@ -271,7 +271,7 @@
*/
extraPlugins: {
validator: AlloyEditor.Lang.isString,
value: 'ae_uicore,ae_selectionregion,ae_selectionkeystrokes,ae_dragresize,ae_imagealignment,ae_addimages,ae_placeholder,ae_tabletools,ae_tableresize,ae_autolink',
value: 'ae_uicore,ae_selectionregion,ae_selectionkeystrokes,ae_dragresize,ae_imagealignment,ae_addimages,ae_placeholder,ae_tabletools,ae_tableresize,ae_autolink,ae_embed',
writeOnce: true
},

Expand Down Expand Up @@ -339,6 +339,9 @@
value: [{
keys: CKEDITOR.CTRL + 76 /*L*/,
selection: 'link'
}, {
keys: CKEDITOR.CTRL + CKEDITOR.SHIFT + 76 /*L*/,
selection: 'embed'
}]
},

Expand All @@ -363,7 +366,7 @@
validator: '_validateToolbars',
value: {
add: {
buttons: ['image', 'camera', 'hline', 'table'],
buttons: ['image', 'embed', 'camera', 'hline', 'table'],
tabIndex: 2
},
styles: {
Expand Down
230 changes: 230 additions & 0 deletions src/ui/react/src/components/buttons/button-embed-edit.jsx
@@ -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;
}());
102 changes: 102 additions & 0 deletions src/ui/react/src/components/buttons/button-embed.jsx
@@ -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;
}());

0 comments on commit 6fc3959

Please sign in to comment.