-
-
Notifications
You must be signed in to change notification settings - Fork 835
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Like previous "state PRs", this moves app-wide logic relating to our "composer" widget to its own "state" class, which can be referenced and called from all parts of the app. This lets us avoid storing component instances, which we cannot do any longer once we update to Mithril v2. This was not as trivial as some of the other state changes, as we tried to separate DOM effects (e.g. animations) from actual state changes (e.g. minimizing or opening the composer). New features: - A new `app.screen()` method returns the current responsive screen mode. This lets us check what breakpoint we're on in JS land without hardcoding / duplicating the actual breakpoints from CSS. - A new `SuperTextarea` util exposes useful methods for directly interacting with and manipulating the text contents of e.g. our post editor. - A new `ConfirmDocumentUnload` wrapper component encapsulates the logic for asking the user for confirmation when trying to close the browser window or navigating to another page. This is used in the composer to prevent accidentally losing unsaved post content. There is still potential for future cleanups, but we finally want to unblock the Mithril update, so these will have to wait: - Composer height change logic is very DOM-based, so should maybe not sit in the state. - I would love to experiment with using composition rather than inheritance for the `ComposerBody` subclasses.
- Loading branch information
1 parent
62a2e84
commit 5e465f6
Showing
18 changed files
with
630 additions
and
396 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
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,37 @@ | ||
import Component from '../Component'; | ||
|
||
/** | ||
* The `ConfirmDocumentUnload` component can be used to register a global | ||
* event handler that prevents closing the browser window/tab based on the | ||
* return value of a given callback prop. | ||
* | ||
* ### Props | ||
* | ||
* - `when` - a callback returning true when the browser should prompt for | ||
* confirmation before closing the window/tab | ||
* | ||
* ### Children | ||
* | ||
* NOTE: Only the first child will be rendered. (Use this component to wrap | ||
* another component / DOM element.) | ||
* | ||
*/ | ||
export default class ConfirmDocumentUnload extends Component { | ||
config(isInitialized, context) { | ||
if (isInitialized) return; | ||
|
||
const handler = () => this.props.when() || undefined; | ||
|
||
$(window).on('beforeunload', handler); | ||
|
||
context.onunload = () => { | ||
$(window).off('beforeunload', handler); | ||
}; | ||
} | ||
|
||
view() { | ||
// To avoid having to render another wrapping <div> here, we assume that | ||
// this component is only wrapped around a single element / component. | ||
return this.props.children[0]; | ||
} | ||
} |
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,109 @@ | ||
/** | ||
* A textarea wrapper with powerful helpers for text manipulation. | ||
* | ||
* This wraps a <textarea> DOM element and allows directly manipulating its text | ||
* contents and cursor positions. | ||
* | ||
* I apologize for the pretentious name. :) | ||
*/ | ||
export default class SuperTextarea { | ||
/** | ||
* @param {HTMLTextAreaElement} textarea | ||
*/ | ||
constructor(textarea) { | ||
this.el = textarea; | ||
this.$ = $(textarea); | ||
} | ||
|
||
/** | ||
* Set the value of the text editor. | ||
* | ||
* @param {String} value | ||
*/ | ||
setValue(value) { | ||
this.$.val(value).trigger('input'); | ||
} | ||
|
||
/** | ||
* Focus the textarea and place the cursor at the given index. | ||
* | ||
* @param {number} position | ||
*/ | ||
moveCursorTo(position) { | ||
this.setSelectionRange(position, position); | ||
} | ||
|
||
/** | ||
* Get the selected range of the textarea. | ||
* | ||
* @return {Array} | ||
*/ | ||
getSelectionRange() { | ||
return [this.el.selectionStart, this.el.selectionEnd]; | ||
} | ||
|
||
/** | ||
* Insert content into the textarea at the position of the cursor. | ||
* | ||
* @param {String} text | ||
*/ | ||
insertAtCursor(text) { | ||
this.insertAt(this.el.selectionStart, text); | ||
|
||
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true })); | ||
} | ||
|
||
/** | ||
* Insert content into the textarea at the given position. | ||
* | ||
* @param {number} pos | ||
* @param {String} text | ||
*/ | ||
insertAt(pos, text) { | ||
this.insertBetween(pos, pos, text); | ||
} | ||
|
||
/** | ||
* Insert content into the textarea between the given positions. | ||
* | ||
* If the start and end positions are different, any text between them will be | ||
* overwritten. | ||
* | ||
* @param start | ||
* @param end | ||
* @param text | ||
*/ | ||
insertBetween(start, end, text) { | ||
const value = this.el.value; | ||
|
||
const before = value.slice(0, start); | ||
const after = value.slice(end); | ||
|
||
this.setValue(`${before}${text}${after}`); | ||
|
||
// Move the textarea cursor to the end of the content we just inserted. | ||
this.moveCursorTo(start + text.length); | ||
} | ||
|
||
/** | ||
* Replace existing content from the start to the current cursor position. | ||
* | ||
* @param start | ||
* @param text | ||
*/ | ||
replaceBeforeCursor(start, text) { | ||
this.insertBetween(start, this.el.selectionStart, text); | ||
} | ||
|
||
/** | ||
* Set the selected range of the textarea. | ||
* | ||
* @param {number} start | ||
* @param {number} end | ||
* @private | ||
*/ | ||
setSelectionRange(start, end) { | ||
this.el.setSelectionRange(start, end); | ||
this.$.focus(); | ||
} | ||
} |
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
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
Oops, something went wrong.