Can I control the power of a manual flash with the help of a trigger?

So I have:

  • YN 560 IV which has both an integrated transmitter and receiver.
  • Neweer TT560 Manual Flash
  • Yongnuo RF 603II C3
    I can have the YN on top of my camera and the Neweer on one foot with the attached RF 603.

My question is this: since the YN 560 IV can control other wireless flashes, can it also control and group the Neweer TT560 flash since it is connected to a wireless receiver now?

[WTS] Warez-Host.Com – Offshore XEN and OpenVZ VPS Europe – 15% discount

Hi everybody

Warez-Host was founded in 2007 and offers high quality, reliable and affordable offshore hosting services.
We have rock solid infrastructure and we are sure to serve you in the next few years.

Warez-Host has adapted its standard VPS server plans to the needs of web application developers, businesses and resellers of web hosting.

All servers are fully managed by our team of experts according to your request.

VPS features:[/COLOR]

[COLOR=RoyalBlue]*[/COLOR] Xeon E5-2440 v2 dual processor server, 64 GB DDR3 RAM
[COLOR=RoyalBlue]*[/COLOR] Xeon E5-2620 v4 dual processor server, 64 GB DDR3 RAM
[COLOR=RoyalBlue]*[/COLOR] SSD disk (SSD servers)
[COLOR=RoyalBlue]*[/COLOR] More than 12 years of experience in the hospitality industry
[COLOR=RoyalBlue]*[/COLOR] Premium Support 24x7x365
[COLOR=RoyalBlue]*[/COLOR] Live chat support
[COLOR=RoyalBlue]*[/COLOR] 99.9% uptime guarantee
[COLOR=RoyalBlue]*[/COLOR] 1 Gbps dedicated uplink port
[COLOR=RoyalBlue]*[/COLOR] Full access to the root
[COLOR=RoyalBlue]*[/COLOR] Competitive prices
[COLOR=RoyalBlue]*[/COLOR] Fully managed servers
[COLOR=RoyalBlue]*[/COLOR] OpenVZ / Xen Virtualizations
[COLOR=RoyalBlue]*[/COLOR] SolusVM Control Panel
[COLOR=RoyalBlue]*[/COLOR] Real rDNS
[COLOR=RoyalBlue]*[/COLOR] 24×7 monitoring

[COLOR=RoyalBlue] Network and facilities:[/COLOR]
[COLOR=RoyalBlue]*[/COLOR] Fast, secure and stable network
[COLOR=RoyalBlue]*[/COLOR] High performance servers in our private colocation in Europe
[COLOR=RoyalBlue]*[/COLOR] Network Location: Netherlands / Bulgaria / Sweden / Russia


Linux VPS plans with discounted prices:

VPS-NL / BG / SE / RU 1
[COLOR=Red]-[/COLOR] Storage: 30 GB
[COLOR=Red]-[/COLOR] Guaranteed memory: 1024 MB
[COLOR=Red]-[/COLOR] 2 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 1000 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden / Russia
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] OpenVZ / Xen
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 17[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]
Order now – [COLOR=RoyalBlue]Sweden[/COLOR]

VPS-NL / BG / SE / RU 2
[COLOR=Red]-[/COLOR] Storage: 50 GB
[COLOR=Red]-[/COLOR] Guaranteed memory: 2048 MB
[COLOR=Red]-[/COLOR] 2 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 1500 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden / Russia
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] OpenVZ / Xen
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 25.5[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]
Order now – [COLOR=RoyalBlue]Sweden[/COLOR]

VPS-NL / BG / SE / RU 4
[COLOR=Red]-[/COLOR] Storage: 100 GB
[COLOR=Red]-[/COLOR] Guaranteed memory: 4096 MB
[COLOR=Red]-[/COLOR] 4 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 2500 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden / Russia
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] OpenVZ / Xen
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 51[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Russia[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]

Discounted Windows VPS packages:

WS-NL / BG / SE 1
[COLOR=Red]-[/COLOR] Storage: 30 GB
[COLOR=Red]-[/COLOR] Dedicated memory: 1024 MB
[COLOR=Red]-[/COLOR] 2 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 1000 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 23,80[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Sweden[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]

WS-NL / BG / SE 2
[COLOR=Red]-[/COLOR] Storage: 50 GB
[COLOR=Red]-[/COLOR] Dedicated memory: 2048 MB
[COLOR=Red]-[/COLOR] 2 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 1500 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 32,30[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Sweden[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]

WS-NL / BG / SE 3
[COLOR=Red]-[/COLOR] Storage: 70 GB
[COLOR=Red]-[/COLOR] Dedicated memory: 3072 MB
[COLOR=Red]-[/COLOR] 3 processors
[COLOR=Red]-[/COLOR] Monthly bandwidth: 2000 GB
[COLOR=Red]-[/COLOR] Server locations: Netherlands / Bulgaria / Sweden
[COLOR=Red]-[/COLOR] 1 Gbps network connection
[COLOR=Red]-[/COLOR] Fully managed server
[COLOR=Red]-[/COLOR] 2 dedicated IP addresses
~ [COLOR=Red]Monthly price: $ 45.90[/COLOR] (Coupon code: [COLOR=Red]SAVEON15V[/COLOR])
Order now – [COLOR=RoyalBlue]Netherlands[/COLOR]
Order now – [COLOR=RoyalBlue]Sweden[/COLOR]
Order now – [COLOR=RoyalBlue]Bulgaria[/COLOR]

[COLOR=Red]._._._._._._._._._._._._._._._._._._._._._._._. ._._._._._._._._._._._._._._._._._._._.[/COLOR]

[COLOR=RoyalBlue]Operating systems available:[/COLOR]
[COLOR=RoyalBlue]*[/COLOR] CentOS
[COLOR=RoyalBlue]*[/COLOR] Debian
[COLOR=RoyalBlue]*[/COLOR] Ubuntu
[COLOR=RoyalBlue]*[/COLOR] Felt
[COLOR=RoyalBlue]*[/COLOR] Slackware
[COLOR=RoyalBlue]*[/COLOR] Windows 2008
[COLOR=RoyalBlue]*[/COLOR] Windows 2012

[COLOR=RoyalBlue]Control panels available:[/COLOR]
[COLOR=RoyalBlue]*[/COLOR] cPanel / WHM (All addons available)
[COLOR=RoyalBlue]*[/COLOR] DirectAdmin
[COLOR=RoyalBlue]*[/COLOR] Webmin
[COLOR=RoyalBlue]*[/COLOR] Kloxo

[COLOR=RoyalBlue]Methods of payment:[/COLOR]
[COLOR=RoyalBlue]*[/COLOR] Pay Pal
[COLOR=RoyalBlue]*[/COLOR] Credit card
[COLOR=RoyalBlue]*[/COLOR] Transfer
[COLOR=RoyalBlue]*[/COLOR] Skrill (MoneyBookers)
[COLOR=RoyalBlue]*[/COLOR] perfect money
[COLOR=RoyalBlue]*[/COLOR] WebMoney
[COLOR=RoyalBlue]*[/COLOR] BitCoin

If you want to test the performance of our high-speed network, we provide download files and IP addresses to test the speed of our network. Do not hesitate to contact us
sales @ warez-host com

[COLOR=RoyalBlue]Response time:[/COLOR]
We provide a 30 minute guaranteed response time for all support tickets. Most issues will be resolved within 2 hours. Experienced technicians will work on your request to guarantee the minimum resolution time. The average resolution time is 2 hours.

Do not delay, start today!
No contract – Cancel at any time!
Instant installation!
Free website migration!

We can help you with complete data migration, for free!

For more information or to view our additional products and services, visit our website Warez-Host.Com

Need something personalized to your needs? Contact us and we will create a specialized plan just for you!

Warez-Host.Com is a division of IWS Network Solutions
Take a look at what our real customers say about us: warez-host com / php testimonials
If you have any questions, please send an email to sales @ warez-host com

Thank you for taking the time to read our offers.


kafka – Can I control which instances of terraform are removed when reducing the number?

When multi-instance provisioning needs to be reduced, can I control which of the existing resources will be removed?

I have a Kafka cluster with variable payload, so I would like to provide brokers customers with a list of brokers currently in business. For this list, I would like to know which one will be deleted if I reduce the number of terraforms.

[WTS] UK and US Reseller SSD VPS starting at $ 5.5 per month | Linux and Windows RDP


Veeble offers versatile web services, including virtual private servers, dedicated servers, remote desktop solutions, and web hosting. In addition, we are building a narrower and more reliable Web with complementary services to the fastest technology. We offer affordable 24×7 managed services that promise trouble-free hosting.

Presentation VPS Reseller, the ultimate VPS package for you.

»Create and delete VPS at any time
You can deploy Linux and Windows VPS at any time from the Control Panel using the available resources. You can also delete the SMV without hassle.

»Assign VPS to users you create
You can create user accounts and assign the deployed VPS to these accounts. Users can connect and manage the VPS via a separate control panel.

»Control panel with white label
You will be able to provide a control panel with a white label to your users. All you need is a domain name and we will help you with the blank configuration.

»Linux and Windows operating systems
You can deploy VPS with the Linux and Windows operating systems readily available in the Control Panel. There are no separate license fees for Windows operating systems.

»Free connectivity at 1 Gbps
You can benefit from a free 1 Gbps bandwidth on all deployed VPS.

»Multiple locations
Create VPS in the United Kingdom and the United States.

VPS Reseller Packages

»Number of VPS: 4 and more
Total number of processor cores: 8 and over
»Total RAM: 4 GB and more
»Total disk: 40 GB and more
»Number of IPs: 4 and over
Total bandwidth: 4TB and more per month in a 1Gbps port
»US & UK Locations

Starting at only $ 25/month

Set up and order now

————————————————– ————————————————– ————————————————– ————————————————– —————-

Regular VPS packages

Openvz VPS –
Xen VPS –
Windows RDP VPS –
Managed VPS –

————————————————– ————————————————– ————————————————– ————————————————– —————-

We accept the following payment methods

Pay Pal
All major international credit cards
Perfect money
Advanced cash
Bitcoin, Ethereum, Litecoin, Ripple
All Indian credit and debit cards and Netbanking
Electronic Payment Portfolio
Local bank transfer to United States, United Kingdom and Germany
Bank transfer / fast

Learn more about payment methods here –

————————————————– ————————————————– ————————————————– ————————————————– —————-

Our support

We work tirelessly to ensure world-class support and a great customer experience.
You can contact us via

Email / tickets (24×7)
Live Chat (24×7)

————————————————– ————————————————– ————————————————– ————————————————– —————-

Connect with us via social networks


If you have questions, log on to[/QUOTE]


Hosting Control Panel with Apache Jail / Chroot?


I am looking for an accommodation control panel with integrated "Apache Chroot" feature per user.

I have seen Plesk have this feature but I do not want to use oakly / Cpanel owned panels.

Any other panel is available with the built-in Apache Chroot feature?

Mods: It seems like I posted it in a bad section. If so, please move it to the Hosting Control Panel section.

Thank you.

Mac App – Chaos Control 1.5.2 macOS | NulledTeam UnderGround

Chaos control 1.5.2 | macOS | 59 mb

Chaos Control was created to help you manage your goals and desired results, both in your professional and personal life. We are pleased to present a complete Mac OS application that meets all of Chaos Control's expectations: managing your goals, managing tasks, setting reminders, planning, and more.

It's a perfect tool to improve your productivity, your organization and your creativity.
The desktop format is perfect for "tree" navigation that helps you categorize your goals with the help of folders. Note that you can create nested folders to make the structure of your project more convenient (for example, Businesses -> Travel, Business -> Partnerships, etc.):
Chaos Control is a task manager based on the best ideas of the GTD (Getting Things Done) methodology created by David Allen. Whether you're running a business, launching an app, working on a project or just planning your vacation trip, Chaos Control is the perfect tool to manage your goals, juggle your priorities and organize your tasks to get everything done. And the best part is that you can handle both heavy project planning and a simple daily routine like shopping list management in a flexible application. In addition, Chaos Control is available on all major mobile and desktop platforms with seamless synchronization.
: OS X 10.11 or later 64-bit


c – What are the ascii numbers for shift and control?

I'm trying to create a program that will detect when certain keys are pressed. My only problem is that I do not know the users who correspond to the Shift and Control keys. I write this code for a game. Here are the codes:

switch (event-> keyval) {
Case ?:
printf ("Shift key was pressed");
printf ("The navigation key has been pressed");

What can replace ""? "" And "" ?? ""

[WTS] solid and decent with quality support.

This is a discussion on solid and decent with quality support. in the Webmaster Marketplace forums, part of the enterprise category; KVC Hosting was launched in 2010 for the sole purpose of creating a host society that is affordable for everyone. …


Remote Control – Make Android Phone Responds to Bluetooth Terminal Commands

I'm developing a toy. The toy has an arduino nano and bluetooth module HC05.
The toy sends data to the connected Android phone via Bluetooth.
I could understand how to get the HC05 data strings on the phone.
How to make the phone play a song / mp3 file after receiving specific data strings from the toy?

This is possible? Is there an application for this?

I have seen articles recommending Airdroid to play sounds remotely, but it does not receive commands from a Bluetooth terminal. Another related question was not answered.

Thank you.

react.js – Emulation of SE text input control for tags

I've used React to write a component, which is supposed to be a faithful emulation of the input of "Tags" that you see at the top of every question / topic on SE.

"This is an emulation of the Tag Editor" defines the functional specification – or refer also to the "Appearance and Behavior" section at the top of the component's README file, included below.

It works here if you want to try it:

Can you review, comment on the implementation?

I guess my own comments / questions are:

  • It is longer than I would have imagined when I started. But it can not be shorter, is not it?
  • There are a lot of comments – too much, not enough, could they be better?
  • What about the README file, namely the file?
  • Is there a change that would make it easier to understand – before first reading, at first reading and / or during replay or maintenance?
  • Is it "idiomatic"? It builds itself without errors eslint and runs without assertion errors, but I have not seen a lot of previous React / TypeScript code written by other people, so I do not know it not.
  • Do people write components like this using JavaScript, without TypeScript? I've refactored it, while I was developing improvements to previous designs that were failing in different ways, and I still assume it's harder or longer to refactor without the compiler's diagnostics .
  • Having as a map (a table of contents) at the top of the file – that is, a list of type names and function names – seems very unconventional and a big clue that the file may be too long and must be split several named files. But I found it useful in this case, and I'm not sure it would be clearer to split it into several files. If it was split into several files, it would be possible to divide it in different ways (because any module can import any other module). Do you think it has to be split? Does the current code make you say "WTF?"? If so, do you have strong ideas? how / where to partition and in how many separate modules?
  • the MutableState.replaceElement method is not obvious, neither first reading nor later reading
  • It is unfortunate that part of the decision making is in the HandleKeyDown manager instead of just in the reducer (but I do not see how to avoid that)
  • Do you think that there should be automated testing (for regression testing)? Would you like to specify (for example, by showing an example) what might a test case look like? I test it by trying it manually.


import React to 'react';
import & # 39; ./ EditorTags.css & # 39 ;;
// this consists of displaying a small "x" SVG - a Close icon that is displayed on each tag - by clicking on it, the tag will be removed
// also to display a small SVG (!) & # 39; - an error icon that is displayed in the item, if there is a validation error
import * as the "../icons" icon;
// this simply displays red text if non-empty text is passed to its errorMessage property
import {ErrorMessage} from & # 39; / ErrorMessage & # 39 ;;

// these are the properties of an existing tag, used or displayed by the TagDictionary
TagCount interface {
key: string,
summary?: string,
account: number

// these are properties to configure tag validation
Validation of the interface {
// if a minimum number of tags must be defined, eg 1
minimum: boolean,
// if a maximum number of tags must be defined, eg 5
maximum: boolean,
// if the user can create new tags, or if the tags should match what is already in the dictionary
canNewTag: boolean,
// if the error messages show validation - they are hidden until the user presses the form submit button
showValidationError: boolean,
// the href used for linking to "popular tags" in the validation error message - that is, "/ tags"
hrefAllTags: string

// the results are returned to the parent via this reminder
OutputTags export interface {tags: string[], isValid: boolean};
type ParentCallback = (outputTags: OutputTags) => void;

// this defines the properties that you pass to the EditorTags functional component
EditorTagsProps interface extends validation {
// the entry / original tags to edit (or an empty table if there are none)
inputTags: string[],
// the results are returned to the parent via this reminder
result: ParentCallback,
// a function to retrieve all existing tags on the server (for tag dictionary search)
getAllTags: () => Promise

/ *
This source file is long and includes the following sections - see also [EditorTags](./

# Defined outside the component of the React function:

- All type definitions
- to affirm
- ParentCallback
- The context
- State
- RenderedElement
- RenderedState
- InputElement
- InputState
- MutableState
- TagDictionary

- the reducer
- types of action
- reducer

- various functions of assistance
- stringSplice
- log and logRenderedState
- getInputIndex
- getElementStart and getWordStart
- assertElements and assertWords
- getTextWidth
- handleFocus

- Functions that build the RenderedState
- renderState
- Initial state

# Defined inside the component of the React function:

- React the hooks
- Error message
- assert (a function that uses errorMessage and is required by initialState)
- State

- inputRef (data used by some of the event handlers)

- Event Managers (who send to the reducer)
- getContext
- handleEditorClick
- handleDeleteTag
- handleTagClick
- handleChange
- handleKeyDown
- handleHintResult

- Tag is a FunctionComponent to make each tag

- The return statement that gives the JSX.Element from this function component

# Other function components to display drop-down tips

- Show clues
- Unveil an index
* /

// you can temporarily change this to enable logging, for debugging
const isLogging = false;

/ *
All type definitions
* /

type Assert = (assertion: boolean, message: string, extra ?: () => object) => void;

// this is additional data that event handlers pass (as part of the action) from the function component to the gearbox
Interface context {
inputElement: InputElement;
Assert: Assert;
result: ParentCallback;
tagDictionary?: TagDictionary;
validation: validation;

// it's like the input data from which the RenderedState is calculated
// these and other status items are read-only, so event handlers must mutate MutableState
State of the interface {
// the selection range in the buffer
// this can even extend over several words, in which case all the selected words are in the  element
read-only selection: {read-only start: number, read-only end: number},
// the words (that is to say the tags when they are separated by spaces)
read-only buffer: string

// this interface identifies the array of  and  elements to be rendered, and the word associated with each
RenderedElement interface {
// the string value of this word
read-only word: string;
// if this word is rendered by a Tag element or by one of the input elements
readonly type: "tag" | "contribution";
// if this word corresponds to an existing tag in the dictionary
readonly isValid: boolean;

// this interface combines both states, and is what is stored using useReducer
RenderedState {interface
// the buffer that contains the keywords and the selection in the buffer
readonly state: state;
// how is that made that is the  more element  items
readonly elements: ReadonlyArray;
// the current value ("semi-controlled") of the  element
readonly inputValue: string;
// the tips associated with inputValue, taken from TagDictionary
tips: TagCount[];
// the validation error message (zero length if there is none)
validationError: string;

// this wraps up the current state of the  control
class InputElement {
readonly selectionStart: number;
readonly selectionEnd: number;
read-only isDirectionBackward: boolean;
readonly value: string;
readonly isLeftTrimmed: boolean;
private readonly inputElement: HTMLInputElement;

constructor (inputElement: HTMLInputElement, assert: Assert, stateValue ?: string) {

let {selectionStart, selectionEnd, selectionDirection, value} = inputElement;
log ("getInput", {selectionStart, selectionEnd, selectionDirection, value});

assert (! stateValue || stateValue === value, "stateValue! == value");

// The TypeScript statement says that these can be null, although I have not seen it in practice?
if (selectionStart === null) {
assert (false, unexpected unexpected "unexpected null selection");
selectionStart = 0;
if (selectionEnd === null) {
assert (false, "selectionEnd null unexpected");
selectionEnd = 0;
if (selectionStart> selectionEnd) {
assert (false, "unexpected selectionStart> selectionEnd");
selectionStart = 0;
// trim left if user enters start spaces
let isLeftTrimmed = false;
while (value.length && value[0] === "" {
value = value.substring (1);
isLeftTrimmed = true;

this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
this.isDirectionBackward = selectionDirection === "backward";
this.value = value;
this.isLeftTrimmed = isLeftTrimmed;
this.inputElement = inputElement;

focus (): void {
this.inputElement.focus ();

setContent (value: string, start: number, end: number): void {
// set the value before the selection, otherwise the selection might be invalid
this.inputElement.value = value;
this.inputElement.setSelectionRange (start, end);
// dynamically adjusts the width of the input element so that it matches its contents
const width = getTextWidth (value + "0"); = `$ {width} px`;

toJSON (): string {
// JSON.stringify can not handle `inputElement: HTMLInputElement`, the purpose of this is to exclude this
printable const: string[] = [
      `start: ${this.selectionEnd}`,
      `end: ${this.selectionEnd}`,
      `backward: ${this.isDirectionBackward}`,
      `value: ${this.value}`
returns printable.join (",");

// that combines the state of the  control with the position of the  in the RenderedState
// it exists only to help KeyDown event handlers determine if keys such as ArrowLeft will change the selected word
// beware that when it works,  The value of the control may not have been written to the elements yet[editing].word
InputState class {
readonly canMoveLeft: boolean;
readonly canMoveRight: boolean;
readonly currentStart: number;
readonly currentEnd: number;
get nextLeft (): number {return this.currentStart - 1; }
get nextRight (): number {return this.currentEnd + 1; }

constructor (status: RenderedState, inputElement: InputElement, assert: Assert) {
const {elements} = state;
const {inputIndex, isFirst, isLast} = getInputIndex (elements, assert);
const elementStart = getElementStart (elements, inputIndex, assert);

const {selectionStart, selectionEnd, isDirectionBackward, value} = inputElement;

// if a track is selected, which end of the track is moving?
const isLeftMoving = (selectionStart === selectionEnd) || isDirectionBackward;
const isRightMoving = (selectionStart === selectionEnd) || ! isDirectionBackward;

// can go left if at the beginning of the  and there are others  elements before the  element
this.canMoveLeft = selectionStart === 0 &&! isFirst && isLeftMoving;
// can go right if at the end of the  and there are others  elements after the  element
this.canMoveRight = selectionEnd === value.length &&! isLast && isRightMoving;

this.currentStart = elementStart + selectionStart;
this.currentEnd = elementStart + selectionEnd;

removeSelected (mutableState: MutableState): void {
mutableState.remove (this.currentStart, this.currentEnd);

// this is a class that event handlers use to mutate the current state
// its methods are the primitive methods required by event handlers that use it
// it is built from the previous RenderedState, then mutated, then returns the new RenderedState
MutableState class {
private selection: {start: number, end: number};
private buffer: string;
// stores the elements as a word because until the end of the mutation, we do not know what will be the entry element
// for example. the current entry element can be deleted and / or the entry can be moved to another word
private words: string[];
private context: context;

constructor (renderState: RenderedState, context: Context) {
// load the data from the previous state into unread read-only items
const {state, elements} = renderState;
this.selection = state.selection;
this.buffer = state.buffer;
this.words = (x => x.word); // use concat without parameters to convert ReadonlyArray to []
    this.context = context;

// cache entry - that is, update the buffered data to reflect everything that is in the cache  element
const {inputIndex} = getInputIndex (elements, context.assert);
const {value, selectionStart, selectionEnd} = context.inputElement;
this.replaceElement (inputIndex, value, {start: selectionStart, end: selectionEnd});
log ("MutableState", {selection: this.selection, buffer: this.buffer, words: this.words});

// called when the event handler finished transforming this MutableState
getState (): RenderedState {
const state: State = {buffer: this.buffer, selection: this.selection};
const {assert, inputElement, tagDictionary, validation} = this.context;
const renderState: RenderedState = renderState (state, assert, validation, inputElement, tagDictionary);
logRenderedState ("MutableState.getState Inbound", renderState);
// remind the parent to say what are the current tags (excluding empty tags)  word s & # 39; there is one)
const tags: string[] = (element => element.word) .filter (word => !! word.length);
const isValid =! renderState.validationError.length;
this.context.result ({tags, isValid});
return renderState;

replaceElement (index: number, newValue: string, selection ?: {start: number, end: number}): void {
this.invariant ();

const editWord: string = this.words[index];
// a special case is when the input element is empty and the last word - then it exceeds the end of the buffer
const nWords = this.words.length - ((this.words[this.words.length - 1] === ""? ten);

// if the new word matches the existing word, the replacement will not do anything
if (editorWord === newValue) {
const wordStart = getWordStart (this.words, index, this.context.assert);

// optionally insert or delete spaces before or after adding or deleting the word

// if we delete the entire word and that is not the last word, also delete the space after this word
const deleteSpaceAfter = newValue === "" && index <nWords - 1;
if (deleteSpaceAfter) {
this.assertSpaceAt (wordStart + editionWord.length);
// if we delete the last word, delete the space before
const deleteSpaceBefore = newValue === "" && index === nWords - 1 && index! == 0;
if (deleteSpaceBefore) {
this.assertSpaceAt (wordStart - 1);
// if we add another word beyond a previous word (that is, beyond the end of the buffer), insert the space
const addSpaceBefore = (wordStart === this.atBufferEnd ()) && index;
if (addSpaceBefore) {
// state that this word was previously empty and that it is changed to non-empty
this.context.assert (! editWord.length && !! newValue.length, "unexpected at the end of the buffer");

log ("replaceElement", {deleteSpaceAfter, deleteSpaceBefore, addSpaceBefore});
// calculate deleteCount and adjust deleteCount and / or wordStart and / or newValue to insert or delete spaces

const deleteCount: number = editWord.length + (deleteSpaceAfter || deleteSpaceBefore? 1: 0);
const spliceStart: number = (deleteSpaceBefore || addSpaceBefore)? wordStart - 1: wordStart;
const spliceValue: string = (! addSpaceBefore)? newValue: "" + newValue;

// mute the buffer
this.buffer = stringSplice (this.buffer, spliceStart, deleteCount, spliceValue);
// mute the word in the array of elements
if (newValue.length) {
this.words[index] = newValue;
} other {
this.words.splice (index, 1);

// adjust the selected range after the text has been changed
if (selection) {
// called the constructor where the new selection is taken from the  element
const wordStart = getWordStart (this.words, index, this.context.assert);
this.selection.start = selection.start + wordStart;
this.selection.end = selection.end + wordStart;
} other {
// called since onDeleteTag where the existing selection needs to be adjusted to account for the deleted item
if (this.selection.start> wordStart) {
this.selection.start - = deleteCount;
if (this.selection.end> wordStart) {
this.selection.end - = deleteCount;
this.invariant ();

private invariant () {
// we mutate the state but because we make several mutations, this states that the state remains healthy or predictable
assertWords (this.words, this.buffer, this.context.assert);
private assertSpaceAt (index: number) {
this.context.assert (this.buffer.substring (index, index + 1) === "", "wait for a space at this location");

remove (start: number, deleteCount: number): void {
this.buffer = stringSplice (this.buffer, start, deleteCount, "");

// the beginning and end of the selection range are usually the same
setSelectionBoth (where: number): void {
this.selection.start = where;
this.selection.end = where;
setSelectionStart (where: number): void {
this.selection.start = where;
setSelectionEnd (where: number): void {
this.selection.end = where;
// the location of the selection index beyond the end of the buffer (starting with the next empty tag to be set)
atBufferEnd (): number {
return (this.buffer.length)? this.buffer.length + 1: 0;

selectEndOf (index: number) {
const wordStart = getWordStart (this.words, index, this.context.assert);
this.setSelectionBoth (wordStart + this.words[index].length);
concentrate () {
this.context.inputElement.focus ();

// we want to display a maximum of 6 tips
const maxHints = 6;

// this is a class in which to look for indicators for existing tags that match the current input value
TagDictionary class {
// the current implementation repetitively iterates the whole dictionary
// if it's slow (because the dictionary is large) in the future, we could partition the dictionary by letter
private read-only tags: TagCount[];
constructor (tags: TagCount[]) {
this.tags = tags;
getHints (inputValue: string, elements: RenderedElement[]): TagCount[] {
if (! inputValue.length) {
return [];
// do not want what we already have, ie what is the other tags
unwanted const: string[] = elements.filter (x => x.type === "tag"). map (x => x.word);
// we will select the tags in the following priority:
// 1. Tags where inputValue is the beginning of the tag
// 2. If # 1 returns too many tags, then prefer the tags with a higher number because they are the most popular
// 3. If # 1 does not return enough tags, look for the tags for which inputValue matches
// 4. If # 3 returns too many tags, then prefer tags with a higher number
const findTags = (start: boolean, max: number): TagCount[] => {
const found = (this.tags.filter (tag => start
? tag.key.startsWith (inputValue)
: tag.key.substring (1) .includes (inputValue))) filter (x =>! unwanted.includes (x.key));
// count highest first, otherwise alphabetical
found.sort ((x, y) => (x.count === y.count)? x.key.localCompare (y.key): y.count - x.count);
return found.slice (0, max);
const found = findTags (true, maxHints);
return (found.length === maxHints)? found: found.concat (findTags (false, maxHints - found.length));
exists (inputValue: string): boolean {
return this.tags.some (tag => tag.key === inputValue);
toJSON (): string {
It's not worth recording all the elements
return `$ {this.tags.length} elements`;

/ *
The reducer
* /

ActionEditorClick interface {type: "EditorClick", context: context};
interface ActionHintResult {type: "HintResult", context: context, hint: string, entryIndex: number};
interface ActionDeleteTag {type: "DeleteTag", context: context, index: number};
ActionTagClick interface {type: "TagClick", context: context, index: number};
ActionKeyDown interface {type: "KeyDown", context: context, key: string, change key: boolean};
ActionChange interface {type: "Change", context: Context};

type Action = ActionEditorClick | ActionHintResult | ActionDeleteTag | ActionTagClick | ActionKeyDown | ActionChange;

function isEditorClick (action: Action): The action is ActionEditorClick {return action.type === "EditorClick"; }
function isHintResult (action: Action): The action is ActionHintResult {return action.type === "HintResult"; }
function isDeleteTag (action: Action): The action is ActionDeleteTag {return action.type === "DeleteTag"; }
function isTagClick (action: Action): the action is ActionTagClick {return action.type === "TagClick"; }
isKeyDown function (action: Action): The action is ActionKeyDown {return action.type === "KeyDown"; }
function isChange (action: Action): The action is ActionChange {return action.type === "Change"; }

function reducer (status: RenderedState, action: action): RenderedState {

log ("reducer", action);
const inputElement = action.context.inputElement;

// this function returns a MutableState instance, based on the previous state and the transmitted context
// the past context includes the new content of the  element
getMutableState () function: MutableState {
logRenderedState ("getMutableState", state);
returns the new MutableState (state, action.context);
// this function returns an InputState instance
getInputState () function: InputState {
returns new InputState (state, inputElement, action.context.assert);

if (isChange (action)) {
const mutableState: MutableState = getMutableState ();
return mutableState.getState ();

if (isEditorClick (action)) {
// click on the 
=> put the emphasis on the in the
inputElement.focus (); const mutableState: MutableState = getMutableState (); mutableState.setSelectionBoth (mutableState.atBufferEnd ()); return mutableState.getState (); } if (isHintResult (action)) { // click on an index => put the focus on the inputElement.focus (); const mutableState: MutableState = getMutableState (); mutableState.replaceElement (action.inputIndex, action.hint); mutableState.setSelectionBoth (mutableState.atBufferEnd ()); return mutableState.getState (); } if (isDeleteTag (action)) { const mutableState: MutableState = getMutableState (); mutableState.replaceElement (action.index, ""); return mutableState.getState (); } if (isTagClick (action)) { const mutableState: MutableState = getMutableState (); // wants to position the cursor at the end of the selected word mutableState.selectEndOf (action.index); // by clicking on the tag, the entry loses focus mutableState.focus (); return mutableState.getState (); } if (isKeyDown (action)) { const {key, shiftKey} = action; switch (key) { "Home" case: ArrowUp case: { // move the selection to the beginning of the first tag const mutableState: MutableState = getMutableState (); mutableState.setSelectionBoth (0); return mutableState.getState (); } "End" case: "ArrowDown" case: { // move the selection towards the end of the last tag const mutableState: MutableState = getMutableState (); mutableState.setSelectionBoth (mutableState.atBufferEnd ()); return mutableState.getState (); } "ArrowLeft" case: { const inputState: InputState = getInputState (); // we are to the left of the entry so let's cross in the previous tag const mutableState: MutableState = getMutableState (); const wanted = inputState.nextLeft; if (shiftKey) { mutableState.setSelectionStart (wanted); } other { mutableState.setSelectionBoth (wanted); } return mutableState.getState (); } ArrowRight case: { const inputState: InputState = getInputState (); // we are to the right of the entry so let's cross in the next tag const mutableState: MutableState = getMutableState (); const wanted = inputState.nextRight; if (shiftKey) { mutableState.setSelectionEnd (wanted); } other { mutableState.setSelectionBoth (wanted); } return mutableState.getState (); } case "Backspace": { // identical to ArrowLeft except that delete the space between the two tags const inputState: InputState = getInputState (); const mutableState: MutableState = getMutableState (); if (shiftKey) { // also delete everything that is selected inputState.removeSelected (mutableState); } const wanted = inputState.nextLeft; mutableState.remove (wanted, 1); mutableState.setSelectionBoth (wanted); return mutableState.getState (); } "Delete" box: { // identical to ArrowRight, except that the space between the two tags is also deleted const inputState: InputState = getInputState (); const mutableState: MutableState = getMutableState (); if (shiftKey) { // also delete everything that is selected inputState.removeSelected (mutableState); } const wanted = inputState.currentStart; mutableState.remove (wanted, 1); mutableState.setSelectionBoth (wanted); return mutableState.getState (); } default: Pause; } // switch } // if isKeyDown logRenderedState ("reducer returning the old state", state); return condition; } // reducer / * Various support functions * / // Help function similar to Array.splice - the string has an integrated slice but no splice // function stringSplice (text: string, start: number, deleteCount: number, insert: string): string { // up to the beginning but not included const textStart = text.substring (0, start); const textEnd = text.substring (start + deleteCount); const after = textStart + insert + textEnd; log ("stringSplice", {text, start, deleteCount, insert, after}); return after; } function log (title: string, o: object, force?: boolean): void { if (! isLogging &&! force) { return; } const json = JSON.stringify (o, null, 2); console.log (`$ {title} - $ {json}`); } function logRenderedState (title: string, renderState: RenderedState): void { log (title, renderState); } // identify the one-and-only index element in the RenderedElement array getInputIndex function (elements: ReadonlyArray, affirm: affirm) : {inputIndex: number, isFirst: boolean, isLast: boolean} { let inputIndex: number = 0; leave counted = 0; for (let i = 0; i <elements.length; ++ i) { if (elements[i].type === "input") { ++ counted; inputIndex = i; } } assert (counted === 1, "expect exactly one entry element") return {inputIndex, isFirst: inputIndex === 0, isLast: inputIndex === elements.length - 1}; } // getElementStart and assertElements have two versions // that is, that they work with ReadonlyArray or ReadonlyArray // this is because the MutableState class works with ReadonlyArray instead of ReadonlyArray // because he does not know the type associated with each word yet // Help function to determine the offset in the buffer associated with a given RenderedElement element getElementStart function (elements: ReadonlyArray, index: number, assert: Assert): number { returns getWordStart ( (x => x.word), index, assert); } getWordStart function (words: ReadonlyArray, index: number, assert: Assert): number { leave wordStart = 0; for (ie i = 0; i <index; ++ i) { word const = words[i]; // +1 because there is a space between after each word wordStart + = word.length + 1; // affirm that all words are significant (and visible) // it's okay if the last word is empty ie if the the element is beyond the end of the buffer // this would not trigger this statement because we only test for all i <index assert (!! word.length, "getWordStart unexpected zero-length word"); } return wordStart; } // state that the state is as expected function assertElements (elements: ReadonlyArray, buffer: string, assert: Assert): void { assertWords ( (x => x.word), buffer, assert); getInputIndex (elements, assert); elements.forEach ((element, index) => assert (!! element.word.length || (element.type === "input" && index === elements.length - 1), "unexpected zero-length word")); } assertWords function (words: ReadonlyArray, buffer: string, assert: Assert): void { for (i = 0; i < words.length; ++i) { const word = words[i]; const wordStart = getWordStart(words, i, assert); const fragment = buffer.substring(wordStart, wordStart + word.length); assert(word === fragment, "assertElements", // don't bother to call JSON.stringify unless the assertion has actually failed () => {return {word, fragment, wordStart, length: word.length, buffer, words}}); } } function getTextWidth (text: string) { // const getContext = (): CanvasRenderingContext2D | undefined => { if (! (getTextWidth as any) .canvas) { const canvas = document.createElement ("canvas"); const context = canvas.getContext ("2d"); if (! context) { return undefined; } // corresponds to the font family and font size defined in App.css context.font = 14px Arial, "Helvetica Neue", Helvetica, sans serif & # 39 ;; (getTextWidth as any) .canvas = canvas; (getTextWidth as any) .context = context; } return ((getTextWidth as any) .context) as CanvasRenderingContext2D; } const context = getContext (); if (! context) { returns 20; } return context.measureText (text) .width; } // see [Simulating `:focus-within`](./ function handleFocus (e: React.FocusEvent, hasFocus: boolean) { function isElement (related: EventTarget | HTMLElement): related is HTMLElement { return (linked to HTMLElement) .tagName! == undefined; } // read it target const =; const relatedTarget = e.relatedTarget; // relatedTarget is of type EventTarget - conversion upstream of it in HTMLElement bound const: HTMLElement | undefined = (relatedTarget && isElement (relatedTarget))? relatedTarget: undefined;   // récupère le tagName et le className de l&#39;élément   const relatedName = (! relatedTarget)? "! cible": (! liée)? "! element": related.tagName;   const relatedClass = (! related)? "": related.className;   // le connecte   const activeElement = document.activeElement;   const targetName = target.tagName;   const activeElementName = (activeElement)? activeElement.tagName: "! activeElement";   log ("handleFocus", {hasFocus, targetName, activeElementName, relatedName, relatedClass});   // le calcule   hasFocus = hasFocus || (relatedClass === "Indice");   // écrit le résultat   const div = document.getElementById ("tag-both") !;   si (hasFocus) {     div.className = "concentré"; } other {     div.className = ""; } } / *   Fonctions qui construisent le RenderedState * / // cette fonction calcule un nouvel état RenderedState et définit le contenu et la sélection InputElement pour une valeur d&#39;état donnée. // il est appelé depuis initialState et depuis MutableState fonction renderState (state: State, assert: Assert, validation: Validation,   inputElement ?: InputElement, tagDictionary ?: TagDictionary)   : RenderedState {   Eléments const: RenderedElement[] = [];   laisser édition: nombre | undefined = undefined;   let inputValue: string = "";   function setInput (texte: chaîne, début: nombre, fin: nombre, inputElement: InputElement): void {     log ("setInput", {text, start, end});     inputElement.setContent (text, start, end);     inputValue = text;     assert (start> = 0 && end <= text.length, `setInput invalid range: ${text} ${start} ${end}`) } function addElement(type: "tag" | "input", word: string): void { const isValid: boolean = !word.length || validation.canNewTag || (!!tagDictionary && tagDictionary.exists(word)); elements.push({ type, word, isValid }); } // split the buffer const words: string[] = state.buffer.split(" ").filter(word => longueur de mot);   const selection = state.selection;   // c&#39;est ici que chaque mot commence, un index dans le tampon   laissez wordStart = 0;   // cela accumule les mots précédents dans la sélection, lorsque la sélection est une plage qui couvre plus d&#39;un mot   laisser accumulé: {wordStart: number, start: number, text: string} | undefined = undefined;   pour (let wordIndex = 0; wordIndex < words.length; ++wordIndex) { const word = words[wordIndex]; // e.g. if a word's length is 1, then the positions within this word are 0 (start) and 1 (end) const wordEnd = wordStart + word.length; if ((selection.start > wordEnd) || (selection.end < wordStart)) { // selection is not in this word // - selection starts beyond the end of the word // - or selection ends before the start of the word addElement("tag", word); } else { if (!inputElement) { // the initialState function should set the selection at the end of the buffer // i.e. beyond any words (if there are any words) // or at the start of the buffer if there are no words, // so that it isn't necessary to set the selection inside the input element // given that the input element hasn't been created in the DOM yet assert(false, "invalid initial state") continue; } // selection includes some of this word // - selection starts on or before the end of the word // - or selection ends on or after the start of the word if (selection.start >= wordStart) {         // la sélection commence dans ce mot         if (selection.end <= wordEnd) {           // la sélection commence et finit par ce mot           setInput (word, selection.start - wordStart, selection.end - wordStart, inputElement);           montage = éléments.longueur;           addElement ("input", mot); } other {           // commence par ce mot mais se termine par un mot futur           assert (! accumulated, "ne devrait rien accumuler auparavant")           accumulated = {wordStart, start: selection.start - wordStart, text: word}; } } other {         // la sélection a commencé avant ce mot         si (! accumulé) {           assert (false, "aurait dû accumuler quelque chose précédemment"); Carry on; // c&#39;est mauvais mais c&#39;est mieux que le référencement accumulé quand c&#39;est indéfini }         // ajoute à ce qui est déjà accumulé, y compris les espaces entre mots         accumulated.text + = "" + word;         if (selection.end <= wordEnd) {           // la sélection se termine par ce mot           setInput (accumulated.text, accumulated.start, selection.end - accumulated.wordStart, inputElement);           montage = éléments.longueur;           addElement ("input", accumulated.text);           accumulé = non défini; } other {           // la sélection se termine par un mot futur (et nous avons déjà ajouté ce mot à l&#39;accumulateur) } } }     wordStart + = word.length + 1; }   if (typeof édition === "non défini") {     // on n&#39;a pas poussé le élément encore, alors poussez-le maintenant     montage = éléments.longueur;     // if (initialisation), alors les valeurs `input` et` inputRef` n&#39;ont pas encore été créées car l&#39;état est créé     // before they are, via the call to initialState -- but when it is created it&#39;s initially empty so that&#39;s alright     if (inputElement) {       // the element is already part of the DOM; réinitialiser maintenant       setInput("", 0, 0, inputElement); }     addElement("input", ""); }   assertElements(elements, state.buffer, assert);   const hints: TagCount[] = !tagDictionary ? [] : tagDictionary.getHints(inputValue, elements);   // if logging only log the keyword of each hint, otherwise logging the tags&#39; summaries makes it long and hard to read   (hints as any).toJSON = () => "[" + => hint.key).join(",") + "]";   function getValidationError(): string {     const nWords: number = elements.filter(element => !!element.word.length).length;     const invalid: string[] = elements.filter(element => !element.isValid).map(element => element.word);     if (nWords < 1 && validation.minimum) { return "Please enter at least one tag;"; } if (nWords > 5 && validation.maximum) {       return "Please enter a maximum of five tags."; }     if (!!invalid.length) {       return (invalid.length === 1) ? `Tag &#39;${invalid[0]}&#39; does not match an existing topic.`         : `Tags ${ => "&#39;" + word + "&#39;").join(" and ")} do not match existing topics.` } return ""; }   const validationError = getValidationError();   const renderedState: RenderedState = { state, elements, inputValue, hints, validationError };   return renderedState; } // this function calculates the initial state, calculated from props and used to initialize useState function initialState(assert: Assert, inputTags: string[], validation: Validation): RenderedState {   assert(!inputTags.some(found => found !== found.trim()), "input tags not trimmed", () => { return { inputTags }; });   const buffer = inputTags.join(" ");   const start = buffer.length + 1;   const state: State = { buffer, selection: { start, end: start } };   log("initialState starting", { inputTags })   const renderedState: RenderedState = renderState(state, assert, validation);   logRenderedState("initialState returning", renderedState)   return renderedState; } /*   EditorTags -- the functional component * / export const EditorTags: React.FunctionComponent = (props) => {   const { inputTags, result, getAllTags } = props;   const validation: Validation = props;   /*     React hooks * /   // this is an optional error message   const [errorMessage, setErrorMessage] = React.useState(undefined);   function assert(assertion: boolean, message: string, extra?: () => object): void {     if (!assertion) {       if (extra) {         const o: object = extra();         const json = JSON.stringify(o, null, 2);         message = `${message} -- ${json}`; }       // write to errorMessage state means it&#39;s displayed by the `` element       setTimeout(() => {         // do it after a timeout because otherwise if we do this during a render then React will complain with:         // "Too many re-renders. React limits the number of renders to prevent an infinite loop."         setErrorMessage(message);       }, 0);       console.error(message); } }   // see ./ and the definition of the RenderedState interface for a description of this state   // also says that type is infered from signature of reducer   const [state, dispatch] = React.useReducer(reducer, inputTags,     (inputTags) => initialState(assert, inputTags, validation));   // this is a dictionary of existing tags   const [tagDictionary, setTagDictionary] = React.useState(undefined);   logRenderedState("--RENDERING--", state);   // useEffect to fetch all the tags from the server exactly once   // React&#39;s elint rules demand that getAllTags be specified in the deps array, but the value of getAllTags   // (which we&#39;re being passed as a parameter) is utimately a function at module scope, so it won&#39;t vary   React.useEffect(() => {     // get tags from server     getAllTags()       .then((tags) => {         // use them to contruct a dictionary         const tagDictionary: TagDictionary = new TagDictionary(tags);         // save the dictionary in state         setTagDictionary(tagDictionary); })       .catch((reason) => {         // alarm the user         setErrorMessage(`getAllTags() failed -- ${reason}`);       }); } [getAllTags])   /*     inputRef (data which is used by some of the event handlers) * /   const inputRef = React.createRef();   /*     Event handlers (which dispatch to the reducer) * /   function getContext(inputElement: HTMLInputElement): Context {     return { inputElement: new InputElement(inputElement, assert), assert, result, tagDictionary, validation }; }   function handleEditorClick(e: React.MouseEvent) {     const isDiv = ( as HTMLElement).tagName === "DIV";     if (!isDiv) {       // this wasn&#39;t a click on the
itself, presumably instead a click on something inside the div return; }     dispatch({ type: "EditorClick", context: getContext(inputRef.current!) }); }   function handleDeleteTag(index: number, e: React.MouseEvent) {     dispatch({ type: "DeleteTag", context: getContext(inputRef.current!), index });     e.preventDefault(); }   function handleTagClick(index: number, e: React.MouseEvent) {     dispatch({ type: "TagClick", context: getContext(inputRef.current!), index });     e.preventDefault(); }   function handleChange(e: React.ChangeEvent) {     dispatch({ type: "Change", context: getContext( }); }   function handleKeyDown(e: React.KeyboardEvent) {     if (e.key === "Enter") {       // do nothing and prevent form submission       e.preventDefault(); return; }     // apparently dispatch calls the reducer asynchonously, i.e. after this event handler returns, which will be too     // late to call e.preventDefault(), and so we need two-stage processing, i.e. some here and some inside the reducer:     // - here we need to test whether the action will or should be handled within the reducer     // - later in the reducer we need to actually perform the action     function newinputState() {       const inputElement: InputElement = new InputElement( as HTMLInputElement, assert);       return new InputState(state, inputElement, assert); }     function isHandled(): boolean {       switch (e.key) {         case "Home":         case "ArrowUp":           // move selection to start of first tag           return !getInputIndex(state.elements, assert).isFirst;         case "End":         case "ArrowDown":           // move selection to end of last tag           return !getInputIndex(state.elements, assert).isLast;         case "ArrowLeft":         case "Backspace": {           const inputState: InputState = newinputState();           return inputState.canMoveLeft; }         case "ArrowRight":         case "Delete": {           const inputState: InputState = newinputState();           return inputState.canMoveRight; }         default: Pause;       } // switch returns false; }     if (isHandled()) {       e.preventDefault();       const context: Context = getContext( as HTMLInputElement);       dispatch({ type: "KeyDown", context, key: e.key, shiftKey: e.shiftKey }); } }   function handleHintResult(outputTag: string) {     const { inputIndex } = getInputIndex(state.elements, assert);     dispatch({ type: "HintResult", context: getContext(inputRef.current!), hint: outputTag, inputIndex }); }   /*     Tag is a FunctionComponent to render each tag * /   interface TagProps { text: string, index: number, isValid: boolean };   const Tag: React.FunctionComponent = (props) => {     const { text, index, isValid } = props;     //     // eslint-disable-next-line     const close = handleDeleteTag(index, e)} title="Remove tag">;     const className = isValid ? "tag" : "tag invalid"; return handleTagClick(index, e)}>       {text}       {close} }   /*     The return statement which yields the JSX.Element from this function component * /   function showValidationResult() {     const showError = props.showValidationError && !!state.validationError.length;     if (!showError) {       return { className: "tag-editor", icon: undefined, validationError: undefined }; }     const className = "tag-editor invalid validated";     const icon = ;     const validationErrorMessage = state.validationError;     // use instead of --     const suffix = (validationErrorMessage[validationErrorMessage.length - 1] !== ";") ? undefined : ( {"see a list of "} popular tags{"."} )     const validationError =

{validationErrorMessage} {suffix}

;     return { validationError, icon, className }; }   const { validationError, icon, className } = showValidationResult();   function getElement(element: RenderedElement, index: number): React.ReactElement {     const isValid = !props.showValidationError || element.isValid;     return (element.type === "tag") ? : handleFocus(e, true)} onBlur={e => handleFocus(e, false)} /> }   return (
{}         {icon}
) } /*   ShowHints * / interface ShowHintsProps {   // hints (from dictionary)   hints: TagCount[],   // the current value of the tag in the editor   inputValue: string,   // callback of tag selected from list of hints if user clicks on it   result: (outputTag: string) => void } const ShowHints: React.FunctionComponent = (props) => {   const { hints, inputValue, result } = props;   if (!inputValue.length) { return
; }   return (
{!hints.length ? "No results found." : => )}
) } interface ShowHintProps {   // hints (from dictionary)   hint: TagCount,   // the current value of the tag in the editor   inputValue: string,   // callback of tag selected from list of hints if user clicks on it   result: (outputTag: string) => void } const ShowHint: React.FunctionComponent = (props) => {   const { hint, inputValue, result } = props;   function getTag(key: string) {     const index = key.indexOf(inputValue);     return ( {(index === -1) ? key : {key.substring(0, index)} {inputValue} {key.substring(index + inputValue.length)} } ) }   // the key with the matched letters highlighted   const tag = getTag(hint.key);   // count the number of times this tag is used elsewhere, if any   const count = (hint.count) ? ×&nbsp;{hint.count} : undefined;   // the summary, if any   const summary = (hint.summary) ?


: undefined;   // a link to more info i.e. the page which defines this tag   function getMore(key: string) {     const icon = ;     // we use here instead of because this link will open a whole new tab, i.e. another instance of this SPA     // in future I think it would be better to reimplement this as a split screen (two-column) view     const anchor = {icon}; return


; }   const more = getMore(hint.key);   return (
result(hint.key)}       onKeyDown={e => { if (e.key === "Enter") result(hint.key); e.preventDefault() }}       onFocus={e => handleFocus(e, true)} onBlur={e => handleFocus(e, false)} >       {tag}       {count}       {summary}       {more}
) }

This is a README for the source file above.

the EditorTags component lets you edit and select the tags associated with a topic.

  • Appearance and behaviour
  • Implementation state data
  • Sequence of definitions
    • Problem constraints
    • Solution as implemented
  • Controlling the element
  • Simulating :focus-within

Appearance and behaviour

It looks like a simple control, which contains multiple words — one word per tag —
  however all words except the currently-selected word have some visible style applied to them.

It&#39;s implemented as a

comme ça:

        function getElement(element: RenderedElement, index: number): React.ReactElement {
    const isValid = !props.showValidationError || element.isValid;
    return (element.type === "tag")
      :  handleFocus(e, true)} onBlur={e => handleFocus(e, false)} />

  return (
{}         {icon}


— and the state.elements array shown above — contains:

  • Exactly one element, in which you edit the currently-selected word
  • One or more React components of my type , which style the other words which you&#39;re not currently editing

the element may be:

  • Alone in the
  • The first or the last element in the
    , either before or after all items
  • Au milieu de la
    , avec elements to its left and right

When you use the cursor keys (including ArrowLeft and ArrowRight) to scroll beyond the edge of
the control, then this component detects that and changes its selection of which tag is currently editable.

Implementation state data

There&#39;s quite a bit of state (i.e. member data) associated with this component:

  • UNE state.buffer string whose value equals the current string or array of words (i.e. tags) being edited
  • UNE state.selection index or range, that identifies which word is currently being edited —
      c&#39;est un beginning and end range, because you can select a range of text,
    for example. by pressing the shift key when you use the cursor keys
  • the items array, which is calculated from the buffer and the selection range, and which identifies which word is
      associé au element and which other words are associated with the elements.
  • the inputValue which identifies the current value of the element
  • UNE tips array which lists the possible tags which might be a match for the input value
  • UNE validationError message if the current tags are invalid and deserve an error message
// this is like the input data from which the RenderedState is calculated
// these and other state elements are readonly so that event handlers must mutate MutableState instead
interface State {
  // the selection range within the buffer
  // this may even span multiple words, in which case all the selected words are in the  element
  readonly selection: { readonly start: number, readonly end: number },
  // the words (i.e. the tags when this is split on whitespace)
  readonly buffer: string

// this interface identifies the array of  and  elements to be rendered, and the word associated with each
interface RenderedElement {
  // the string value of this word
  readonly word: string;
  // whether this word is rendered by a Tag element or by the one input element
  readonly type: "tag" | "input";
  // whether this word matches an existing tag in the dictionary
  readonly isValid: boolean;

// this interface combines the two states, and is what&#39;s stored using useReducer
interface RenderedState {
  // the buffer which contains the tag-words, and the selection within the buffer
  readonly state: State;
  // how that&#39;s rendered i.e. the  element plus  items
  readonly elements: ReadonlyArray;
  // the current ("semi-controlled") value of the  element
  readonly inputValue: string;
  // the hints associated with the inputValue, taken from the TagDictionary
  hints: TagCount[];
  // the validation error message (zero length if there isn&#39;t one)
  validationError: string;

Because there&#39;s a lot of data, and the data elements are inter-related,
  I implement it with useReducer instead of useState.

Sequence of definitions

The sequence in which things are defined in the source file is significant —
  and it&#39;s fragile, i.e. if you don&#39;t do it right then there&#39;s a compiler error about using something before it&#39;s defined,
  or a run-time error about using something with an undefined value.

I use the following strategy:

  • Because the initialState and therefore the renderState functions are called when the State is initialized and
    before inputRef.current exists, this function and anything called from this function cannot reference the state data.
  • To ensure they don&#39;t reference the state data, they&#39;re defined in the EditorTabs.tsx module (for convenience),
      but defined outside the EditorTags function component inside which the state data are defined, so that the compiler
      would error if they were referenced from those functions.

So the following are defined outside the function component:

  • the initialState and renderState the functions
  • Any TypeScript class definitions which these functions use
  • Any other TypeScript type definitions which these functions or classes use — so, for simplicity, every TypeScript
      type definition.
  • Any small helper/utility functions which these functions use — and so, for simplicity, all helper/utility functions
  • Because a reducer should be stateless or pure, it too is defined outside the function component
  • And therefore also the TypeScript type definitions of the action types, and the corresponding user-defined type guards

So the following remain inside the function component:

  • All state data
  • All event handlers (which delegate to the reducer, and which may reference inputRef.current)
  • the to affirm function depends on the setErrorMessage function, which is state — so the to affirm function too is
      defined inside the function component, and is passed as a parameter to any function which needs it.

Data that&#39;s stored inside the function component, and which isn&#39;t stored as state,
  is passed to the reducer in the "action".

interface ActionEditorClick { type: "EditorClick", context: Context };
interface ActionHintResult { type: "HintResult", context: Context, hint: string, inputIndex: number };
interface ActionDeleteTag { type: "DeleteTag", context: Context, index: number };
interface ActionTagClick { type: "TagClick", context: Context, index: number };
interface ActionKeyDown { type: "KeyDown", context: Context, key: string, shiftKey: boolean };
interface ActionChange { type: "Change", context: Context };

All the action types include an InputElement (which is created by the event handler which generates the action),
because the MutableState class (called from the reducer) requires an InputElement, in order to update the State
(including the buffer and the selection) to match the contents of the element.

In fact there&#39;s other data too, which the reducer needs and is passed in the action:

// this is extra data which event handlers pass (as part of the action) from the function component to the reducer
interface Context {
  inputElement: InputElement;
  assert: Assert;
  result: ParentCallback;
  tagDictionary?: TagDictionary;
  validation: Validation;

Controlling the element

the element is a semi-controlled component (see e.g.
  "What are Controlled Components in React?").

There&#39;s an inputRef as well to access to the underlying HTML element, which is used to set the focus, and
  to get and set the selection range within the element, but not to get the value of the element —
  the value of the element is got via its onChange handler (and the value property of the event&#39;s target).

Note that React&#39;s onChange event handler has redefined (non-standard) semantics — i.e. it&#39;s fired after every change,
  and not only when it loses focus.

I say that it&#39;s "semi" controlled, because although its onChange handler writes its value to state …

<input type="text" ref={inputRef} onChange={handleChange} ...

… it does do not have a corresponding value property which might read its value from state …

<input type="text" ref={inputRef} onChange={handleChange} value={state.inputValue} ...

The reason why not is because if the value property is used to write a string into a previously-empty
  input element, then the selection range within the control is automatically pushed to the end of the new string.

This interferes with the desired behaviour of the ArrowRight key, where we want to copy
  the next (to the right) tag into the input control, and set the selection range to the beginning of the control.

So, instead, the setInput function writes into the value property of the underlying HTMLInputElement.

  • I worried that doing this might trigger another onChange event, but it doesn&#39;t seem to.
  • An alternative might be to use useEffect to alter the selection of the input (to match the selection specified in
      the state), after it&#39;s rendered.
      That seems like even more of a kluge, though — making it "semi-controlled" instead, i.e. writing to the DOM element,
      seems neater.

the inputValue element also still exists as an alement of the RenderedState data,
  but it&#39;s write-only — i.e. it&#39;s up-to-date (and a opy of what was written into the DOM element).

Simulating :focus-within

There are a couple of effects in EditorTags.css for example …

.tag-hints {
  visibility: hidden;

.tag-both:focus-within .tag-hints {
visibility: visible;

… where is would be convenient to use :focus-within. However that&#39;s not supported by some browsers (e.g. Edge).
  Although the "Create React App" setup include postcss modules, they&#39;re not configurable —
  "You cannot customize postcss rules"
  — and the default configuration doesn&#39;t enable support for postcss-focus-within.

To avoid the complexity or fragility of trying to bypass the CRA default configuration,
  instead I use onfocus and onBlur handlers to simulate :focus-within
(by adding or removing focused the class value).

There&#39;s also complexity in deciding whether something has lost focus.

  • The problem is that, using React&#39;s synthetic events, onBlur fires before onFocus
      so focus seems lost when focus moves from .tag-editor input to one of the .hint elements.
  • The right the right way to support this functionality would be to use the focusin event as described in
      Focus Event Order,
      however React&#39;s synthetic events don&#39;t support focusin —

Solutions like …

… solve this using a timer in some way —
  which is probably good, only I&#39;m not certain of the sequence in which events are emitted.

So instead I use a solution (see the handleFocus function) which depends on the relatedTarget of the event:

  • That works on my machine (Windows 10), at least, using Chrome, Firefox, and Edge.
  • warns that apparently this won&#39;t work with IE 9 through IE 11,
      though comments there say that document.activeElement might help make it work on IE

Otherwise, if support for IE (not just Edge) is needed, one of the other solutions listed earlier above might be better.