Browsed by
Category: Personal Development

Making a Chat Bot with wit.ai

Making a Chat Bot with wit.ai

I’m currently building a chat bot with wit.ai as an additional way to search for courses. As part of this I need to learn how to build one.

There are a number of api’s / frameworks available to build chat bots. I am currently using wit.ai

Wit.ai Useful Links

Steps

I followed the Getting Started Guide to create my app : https://wit.ai/getting-started

I then went through the documentation to get a rough idea of how to setup and train my app : https://wit.ai/docs/quickstart

I setup my entities and trained it to certain sentences. (read the quick start documentation to understand how this works)

Finally in the settings tab I have a curl script I can use to test the app in terminal which return:

{
    "_text" : "Do you have any courses in art?",
    "entities" : {
        "course_type" : [
            {
                "confidence" : 0.9968739640596, 
                "value" : "Art",
                "type" : "value" 
            }],
        "sentiment" : [
            {
                "confidence" : 0.87585085150416,
                "value" : "neutral"
            }],
        "intent" : [
            {
                "confidence" : 0.99992613113395,
                "value":"course"
            }]
    },
    "msg_id" : "14PsrGfV9LIEz4neL"
}

Next I need to work out how to integrate this into a website.

More to follow…

Prototypes….

Chat Bot Design Mock

WordPress Cli

WordPress Cli

WP-Cli is the WordPress command line tool. It can be used for updating WordPress, plugins and translations via the terminal.

$ cd /folder/example/
$ wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info
https://make.wordpress.org/cli/handbook/installing/
https://make.wordpress.org/cli/handbook/quick-start/

Updating Translations

$ cd /wp/folder/
$ wp language core update
$ wp language plugin update -all

Updating Plugins

$ cd /wp/folder/
$ wp plugin update --all
Making a plugin for XD

Making a plugin for XD

Development in Progress

I am currently learning and updating this post as I go along so it may change over the next few weeks 🙂 .

XD has this cool little area for adding in plugins to your install. They can be anything from inserting useful elements like calendars, to testing your content is accessible for those with colourblindness.

Then I suddenly noticed a little tab called “Development” under plugins:

So as a challenge I’ve decided to have a go at making my own plugin for XD and make a blog post about it.

Notes

XD plugins use JSON and JavaScript – so not too much of a leap!

A basic XD plugin consists of a manifest.json file and a main.js file. You can also add images and other files but those two files are all that is really needed.

Screenshot of a simple XD plugin folder and the two files required.
A simple XD Plugin

There is also a Quick Start Tutorial for setting up with React (I don’t see much point in reposting their steps 🙂 )

mkdir xdpluginreact
cd xdpluginreact
npm init -y
npm install webpack webpack-cli --save-dev

I also then split the files so there was a distribution folder as I don’t want to include development files in the final version that gets moved to Adobe XD.

I’ve documented that in this post here. Following those steps you end up with:

Once the plugin is finished it is the example-plugin folder that is copied into the Adobe XD develop folder.

Finally I can update the package.json file.
Note : “private”: true means that it can’t be published by accident 🙂

XD Brand / Design Guidelines can be found here : https://github.com/AdobeXD/plugin-design-assets/

Debugging

There is a console you can use for catching error messages.
Go to Plugins -> Development -> Developer Console

Setting a Task / Brief

I generally learn more when I’m trying to do something. So now I’ve got the Quick Start Tutorial working and gone through some of the introduction tutorials + the Quick Start React Tutorial. I’m going to set myself a simple task to learn more.

I’m going to create a Lorem Ipsum generator (I know these already exist but I thought I would start with something that appeared to be simple then expand on it. Plus once I’ve got it working I can try bringing in some more fun Lorem Ipsum Generators rather than the boring standard one).

Requirements

  • Insert Lorem Ipsum into a rectangle
  • Fetch the Lorem Ipsum from a file
  • Get the last selected font settings as default
  • Allow user to change default font settings.

Challenge Requirements

  • Allow user to choose type of Lorem Ipsum.
  • Allow user to change the settings on how much lorem ipsum they need, paragraphs, lines, words e.t.c.
  • Allow User to add their own Lorem Ipsum.

Setting up the Project

I followed the Quick Start Panel UI tutorial to setup the new plugin.

Insert Lorem Ipsum into a rectangle

I modified the manifest.json to include my own information and to change the entry point information.

    "uiEntryPoints": [
        {
            "type": "panel",
            "label": "Insert Lorem Ipsum",
            "panelId": "insertLoremIpsum"
        }
    ]

I then changed the first line of the main.js to include the text scenegraph element.

const { Rectangle, Text, Color } = require("scenegraph");

I replaced the “enlargeRectangle” function in the create function with my own function called “replaceWithLoremIpsum”

 function replaceWithLoremIpsum() {
    const { editDocument } = require("application"); // [3]
    let coords;

    // Edit the Document...
    editDocument({ editLabel: "Insert Lorem Ipsum." }, function(selection) {
        
        // Currently Selected Rectangle. 
        const selectedRectangle = selection.items[0]; // [7]
      
        // Get the Rectangle's Coordinates.
        coords = selectedRectangle.boundsInParent;
      
        // Create a new Text Node
        const node = new Text();
        
        // Text Settings
        node.text = "Lorem Ipsum will go here...";
        node.fill = new Color("#1f1f1f");
        node.fontSize = 50;
        
        // Match the selected Rectangles area and location.
        node.areaBox = {width: coords.width, height: coords.height};
        node.translation = {x: coords.x, y: coords.y }; 
        
        // add the new text box.       
        selection.insertionParent.addChild(node);
        
        // Hide the selected rectangle (keeping it in case the user still needs it. 
        selectedRectangle.visible = false;
        
    });
  }

This function takes the currently selected rectangle,
records the coordinates and dimensions of the rectangle,
creates a new Text node,
Adds some text to the new text node,
Sets some text settings,
Changes the Text node to a fixed width and height that matches the selected rectangle (otherwise it just sets an auto height & width),
Moves the Text area to the same coordinates as the selected rectangle,
Add the new text node to the artboard,
Hides the selected rectangle from the user view.

I decided to keep the rectangle rather than removing it incase the user still needs it. They can always delete it if they prefer.

I then updated the querySelector to use my new function rather than the enlargeRectangle function.

panel.querySelector("form").addEventListener("submit", replaceWithLoremIpsum); // [11]

I also updated the module.exports to match the manifest.json file.

module.exports = {
  panels: {
    insertLoremIpsum: {
      show,
      update
    }
  }
};

Fetch the Text from a file

I want to store the sample text seperately from the code. This is partly to keep the code a bit neater.

I’m using the file reading tutorial as a guide : https://adobexdplatform.com/plugin-docs/tutorials/how-to-read-a-file/
and the File reference : https://adobexdplatform.com/plugin-docs/reference/uxp/storage-index.html
and the documentation about using require to include other files : https://adobexdplatform.com/plugin-docs/reference/javascript/javascript-support.html#can-i-use-require

I’ve created a text folder in the plugin directory with a file called latin.json that has the Lorem Ipsum reference text in it.

{
    "content" : "Lorem ipsum dolor sit amet, conse",
}

I added a variable near the top to act as a container for the selected lorem ipsum.

let textToAdd = "Lorem Ipsum will go here..."; 		 // Container for the Lorem Ipsum text to add...

I added a new function to the create function to handle the generation of the Lorem Ipsum text. I’ve written it in this way as I’m intending to expand on it later.

/**************************************************************
FUNCTION TO FETCH LOREM IPSUM
**************************************************************/
function fetchLoremIpsum() {
		
	const noScentences = 7;	// Total number of scentences to include in a paragraph
	const noParagraphs = 2;	// Total number of paragraphs to include
		
	// Fetch the lorem ipsum.
	const latin = require("./text/latin.json"); 
		
	// Split text into scentences		
	let scentences = latin.content.split('.');
		
	// Clear any existing text
	textToAdd = '';
		
	// keep adding scentences so long as we have enough scentences per paragraph.
	for(let i = 0; i < (noScentences * noParagraphs); i++) {
		textToAdd += scentences[i]+'.';
			
		// if we've reached the total number of scentences per paragraph 
		if(i > 0 && (i % noScentences) === 0) {
			textToAdd += '\n';	// add a line break.
		}
	}
		
}

Then I change the new text node to use the textToAdd variable rather than a set string.

node.text = textToAdd;

Get the last selected font settings as default

I initially wasn’t sure if this would be possible but I thought there probably would be a way to select existing nodes in the document without the user indicating one.

I had a look through the tutorials and felt that the tutorial on changing the colours of rectangles would probably give me some clues on what to do ->
https://adobexdplatform.com/plugin-docs/tutorials/how-to-work-with-scenenodelist/.

I added a new container variable for the last submitted text node settings at the top of main.js

let lastSubmittedText;  // Container for Last Submitted Text Node

I then created a new function in the create function called findTextSettings.

    /**************************************************************
        FUNCTION TO FIND PREVIOUS TEXT SETTINGS
        Parameters that are passed down are Contextual Arguments
        ( https://adobexdplatform.com/plugin-docs/reference/structure/handlers.html#contextual-arguments )
    **************************************************************/  
    function findTextSettings(selection, documentRoot) {
        
        // for each node in the document....
        documentRoot.children.forEach(node => {
            
            // Check if node is a child of Artboard
            if(node instanceof Artboard) {
                let artboard = node;
                
                // Filter out the child nodes that are Text nodes and set as text
                let text = artboard.children.filter(artboardChild => {
                    return artboardChild instanceof Text;
                });
                
                let lastText = text[(text.length - 1)];
                
                // Set last submitted Text settings. 
                lastSubmittedText = {
                    fill : lastText.fill,
                    fontSize : lastText.fontSize
                };
                
            }
            
        });
        
    }

I modified the example function for selecting rectangles in the artboard to select text nodes instead and to update the container variable with the new data.

Note about Contextual Arguments:
https://adobexdplatform.com/plugin-docs/reference/structure/handlers.html#contextual-arguments

The documentRoot parameter needs to be added to the editDcoument function for this to be passed down.

// Edit the Document...
editDocument({ editLabel: "Insert Lorem Ipsum." }, function(selection,documentRoot) {

   findTextSettings(selection, documentRoot);
   // ....

}

I also added a call to update the last submitted text node.

I then modified the text settings of the new text mode to use the last submitted settings if they exist:

// Text Settings
node.text = textToAdd;
node.fill = new Color((typeof lastSubmittedText !== 'undefined'  ? lastSubmittedText.fill : "#1f1f1f"));
node.fontSize = (typeof lastSubmittedText !== 'undefined' ? lastSubmittedText.fontSize : 16);

There are more settings than this so I am now going through and adding all the rest of the settings.

Allow user to change default font settings.

Although fetching the last selected settings is useful as a fallback, I think the user should be able to change the text settings before they import the Lorem Ipsum.

This means adding in some inputs to the main form.

I would like to add a list of system fonts to choose from as well but after some research it appears this is not currently possible. But it is something they may add in this year so I will revisit once it is possible : https://forums.adobexdplatform.com/t/api-to-list-which-fonts-are-available-detect-if-text-has-missing-font/629/4

This is probably the case with fontStyles as well but I’ll add a couple of common ones to the list to choose from.

Once I had modified the form to include the new options I then modified the find TextSettings() function to fetch the user settings.

const userFontSize = Number(document.querySelector("#setFontSize").value);
const userCharSpacing = Number(document.querySelector("#setCharSpacing").value);
const userLineSpacing = Number(document.querySelector("#setLineSpacing").value);
const userParaSpacing = Number(document.querySelector("#setParaSpacing").value);
	    
const userFontStyle = String(document.querySelector("#setFontStyle").value);
const userTextTransform = String(document.querySelector("#setTextTransform").value);
const userTextAlign = String(document.querySelector("#setTextAlign").value);
	    
const userUnderline = Boolean(document.querySelector("input[name='setUnderline']").value);
const userStrike = Boolean(document.querySelector("input[name='setStrike']").value);

Then it was just a case of checking the values and overriding the text settings where the user had set something:

if(userFontSize != '') {
    lastSubmittedText.fontSize = userFontSize;
}
if(userCharSpacing != '') {
    lastSubmittedText.charSpacing = userCharSpacing;
}
if(userLineSpacing != '') {
    lastSubmittedText.lineSpacing = userLineSpacing;
}
if(userParaSpacing != '') {
    lastSubmittedText.paragraphSpacing = userParaSpacing;
}

if(userFontStyle != '' && userFontStyle != 'Choose Font Style') {
    lastSubmittedText.fontStyle = userFontStyle;
}
if(userTextTransform != '' && userTextTransform != 'Choose Text Transform') {
    lastSubmittedText.textTransform = userTextTransform;
}
if(userTextAlign != '' && userTextAlign != 'Choose Text Alignment') {
    lastSubmittedText.textAlign = userTextAlign;
}

if(typeof userUnderline !== "undefined") {
    lastSubmittedText.underline = userUnderline;
}
if(typeof userStrike !== "undefined") {
    lastSubmittedText.strikethrough = userStrike;
}

TroubleShooting / Errors

“missing a name for object member”

Check there isn’t a comma at the end of the JSON (oops)

“Command “JSX” Not Found”

$ sudo npm install -g jsx

“Plugin TypeError: Assignment to constant variable.”

If you define a variable as const you cant change its value. To resolve it just define it by let and try. (oops).

more to follow….

References

https://adobexdplatform.com/plugin-docs/
https://adobexdplatform.com/plugin-docs/tutorials/quick-start/
https://adobexdplatform.com/plugin-docs/tutorials/
https://github.com/AdobeXD/Plugin-Samples

Learning React.js

Learning React.js

I’ve decided to learn how to use react.js in order to create my XD plugin. I felt the best way to start was probably to go through some of React’s basic tutorials and write down my notes as I go along.

The two tutorials are located here :

Create a New React App
React.js Main Tutorial – Tic Tac Toe

I already have node.js and npm installed see Learning Node.js and Webpack

$ npx create-react-app my-app
$ cd my-app
$ npm start

After follow these steps you end up with the following screen:

Screenshot of the local working app.

JSX Note
“Most React developers use a special syntax called “JSX” which makes the react element easier to write.”

https://reactjs.org/tutorial/tutorial.html

Chrome and Firefox have addons for debugging and inspecting React.js componants.

“Immutability makes complex features much easier to implement. Later in this tutorial, we will implement a “time travel” feature that allows us to review the tic-tac-toe game’s history and “jump back” to previous moves. This functionality isn’t specific to games — an ability to undo and redo certain actions is a common requirement in applications. Avoiding direct data mutation lets us keep previous versions of the game’s history intact, and reuse them later.”

“Detecting changes in immutable objects is considerably easier. If the immutable object that is being referenced is different than the previous one, then the object has changed.”

https://reactjs.org/tutorial/tutorial.html

this.state : refers to the current component
this.props : refers to the parent component

Taking it Further

The React tutorial has a few suggestions for taking the tic tac toe game example further. I’m going to have a go at them and see if I can complete them.

Display the location for each move in the format (col, row) in the move history list.

I’m going to modify the history constant to also record the location of the move (so I don’t have to do things like comparing arrays which is probably more complicated).

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                location: null,
            }],
            stepNumber: 0,
            xIsNext: true,
        };
    }
    // Rest is the same...
}

I’ve added in another part “location: null,”

I plan to store the cell key here.

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
        
    if(calculateWinner(squares) || squares[i]) {
        return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
        history: history.concat( [{
            squares: squares,
            location: i
        }]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext,
    });
}

Then in the handleClick(i) I modified the setState to record the location.

Then I change the render() function in the Game class to the following:

render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);
        
    const moves = history.map((step, move) => {
        const desc = move ?
            'Go to move #' + move + ' Location: #' + history[move].location :
            'Go to game start';
        return (
            <li key={move}>
                <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
        );
    });

    // rest is the same....
}

So it is now displaying the location. However the challenge was to show the column and the row so I need to translate that number into the column and row.

I ended up creating an array of all the possible locations in handleClick()

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    const locations = [
        [1,1],
        [2,1],
        [3,1],
        [1,2],
        [2,2],
        [3,2],
        [1,3],
        [2,3],
        [3,3]
    ];
    // ....
}

Then instead of just recording the square key in the location parameter I changed it to the value of the locations array.

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    const locations = [
        [1,1],
        [2,1],
        [3,1],
        [1,2],
        [2,2],
        [3,2],
        [1,3],
        [2,3],
        [3,3]
    ];
        
    if(calculateWinner(squares) || squares[i]) {
        return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
        history: history.concat( [{
            squares: squares,
            location: locations[i]
        }]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext,
    });
        
}

Task one complete.

Bold the currently selected item in the move list.

Easiest way to do this is to put bold tags around the button. The stepNumber keeps track of what move it currently is.

render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);
        
    const moves = history.map((step, move) => {
        const desc = move ?
            'Go to move #' + move + ' Column: ' + history[move].location[0] + ' Row: ' + history[move].location[1] :
            'Go to game start';
        return (
            <li key={move}>
                <button onClick={() => this.jumpTo(move)}>
                    {move == this.state.stepNumber ? <strong>{desc}</strong> : desc}
                </button>
            </li>
        );
    });
}

This checks if the current move number matches the overall step number. If it does it adds some bold tags.

Things I noticed….

You don’t need to enclose the html in quotes
The variable being referenced has to be in one of the parent elements (which makes sense) and it has to be passed from the parent element to the child element.
For some reason the last desc doesn’t need brackets – I’m not sure why this is the case at present. I’m assuming it’s because there’s no html so the brackets just escape the variable from the html string? If there was html I suspect it would need brackets.

Rewrite Board to use two loops to make the squares instead of hardcoding them.

I found this a bit tricker to solve but my solution ended up as follows:

Added a variable to the state of the Game component

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                location: null,
            }],
            stepNumber: 0,
            xIsNext: true,
            cols: 3,
            rows: 3,
        };
    }

    // ....
}

Then modified the Game render() of Board to include the two new variables:

        return (
            <div className="game">
                <div className="game-board">
                    <Board 
                        squares={current.squares}
                        cols={cols}
                        rows={rows}
                        onClick={(i) => this.handleClick(i)}
                    />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{moves}</ol>
                </div>
            </div>
        );

Modified renderSquare to have keys (otherwise you will get warning errors)

    renderSquare(i) {
        return (
            <Square 
                key={i}
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)} 
            />
        );
    }

Added a renderRow function to create each individual row

    renderRow(currentRow) {
        let row = [];
        const count = currentRow * this.props.cols;
        
        for(let i=0; i < this.props.cols; i++) {
            const key = count + i;
            row.push(this.renderSquare(key));
        }
        
        return (
            <div key={currentRow} className="board-row" id={currentRow}>
                {row}
            </div>);
    }

This should recieve the current row numer (e.g. 0, 1 or 2 for each of the 3 rows).

The counter will always start at the current row number times the total number of columbs. e.g.

If the number of columns is 3

0 * 3 = 0 // Start on Cell Number 0
1 * 3 = 3 // Start on Cell Number 3
2 * 3 = 6 // Start on Cell Number 6

A for loop is then run to create the number of columns specified.
Each key is then calculated by the current Cell Number + the current column number.
e.g

Row 0:

Cell Number Start 0 + Column 0 = Key 0
Cell Number Start 0 + Column 1 = Key 1
Cell Number Start 0 + Column 2 = Key 2

Row 1:

Cell Number Start 3 + Column 0 = Key 3
Cell Number Start 3 + Column 1 = Key 4
Cell Number Start 3 + Column 2 = Key 5

Row 2:

Cell Number Start 6 + Column 0 = Key 6
Cell Number Start 6 + Column 1 = Key 7
Cell Number Start 6 + Column 2 = Key 8

Each column then adds a square with the relevant key to an array.
Finally the finished row is returned by the function.

Added a function called renderRows to the Board component:

   renderRows() {
        let gameBoard = [];
        
        for(let i=0; i < this.props.rows; i++) {
            gameBoard.push(this.renderRow(i));
        }
        return gameBoard;
    }

For each row the renderRow function is run and adds the result to the gameBoard array which is then returned by the function.

Finally I edited the render function to the below:

    render() {
            
        return ( 
            <div>
                {this.renderRows()}
            </div>
        );
   }

This has the added bonus of meaning you can expand the board size. However for that to work I need to do a few extra things!

First I need to add a variable for locations to the main Game component.

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                location: null,
            }],
            stepNumber: 0,
            xIsNext: true,
            cols: 3,
            rows: 3,
            locations: [],
        };
    }
    // ...
}

In the Game.render() I also need to add the variable to the Board Component so the Board class can manipulate it.

<Board 
    squares={current.squares}
    cols={cols}
    rows={rows}
    locations={this.state.locations}
    onClick={(i) => this.handleClick(i)}
/>

Then I modify Board.renderRow() to update the locations variable as it creates the board.

    renderRow(currentRow) {
        let row = [];
        const count = currentRow * this.props.cols;
        
        for(let i=0; i < this.props.cols; i++) {
            const key = count + i;
            
            let location = [(currentRow + 1),(i + 1)];
            this.props.locations.push(location);

            row.push(this.renderSquare(key));
        }
        
        return (
            <div key={currentRow} className="board-row" id={currentRow}>
                {row}
            </div>);
    }

Then I updated the Game.handleClick() to use the state variable locations. And also to pass along the number of rows and columns to

    handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        const locations = this.state.locations;
        const rows = this.state.rows;
        const cols = this.state.cols;

        if(calculateWinner(squares, rows, cols) || squares[i]) {
            return;
        }

        // ....
   }

And Finally I updated the CalculateWinner function…

function calculateWinner(squares, rows, cols) {
    
    const wLines = [];
    
    for(let j=0; j < rows; j++) {
        
        let currentRow = j;
        let startCell = currentRow * cols;
        
        for(let k=0; k < cols; k++) {
            
            let key = startCell + k;
            
            // horizontal rows e.g. 0 1 2
            if(key + 2 <= (startCell + (cols - 1))) {
                let winner = [key,(key+1),(key+2)];
                wLines.push(winner);
            }
            
            // vertical columns e.g. 0 3 6
            if((currentRow + 2) <= (rows - 1) ) {
                let winner = [key,(key+cols),(key+(cols * 2))];
                wLines.push(winner);
            }
            
            if((currentRow + 2) <= (rows - 1) ) {
                // diagonals top left to bottom right e.g. 0 4 8
                if(key + 2 <= (startCell + (cols - 1))) {
                    let winner = [key,(key+cols+1), (key+(cols * 2)+2)];
                    wLines.push(winner);
                }
                // diagonals top right to bottom left e.g. 2 4 6
                if( key - 2 >= startCell ) {
                    let winner = [key, (key+cols-1),(key+(cols * 2)-2)];
                    wLines.push(winner);
                }
            }
        }
    }
    
    for( let i=0; i< wLines.length; i++) {
        const [a,b,c] = wLines[i];
        if(squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }
    return null;
}

Add a toggle button that lets you sort the moves in either ascending or descending order.

This one seemed a bit easier. First I added the toggle button to the Game Info, in Game ->render().

<div className="game-info">
    <div>{status}</div>
    <div className="order-moves">
        <button onClick={() => this.reOrderMoves()}>
            Toggle Order {currentOrder}
        </button>
    </div>
    <ol>{moves}</ol>
</div>

Then I added a variable to the state to record the order.

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                location: null,
            }],
            stepNumber: 0,
            xIsNext: true,
            cols: 10,
            rows: 10,
            locations: [], 
            sortOrderDesc: false
        };
    }

    //...
}

Then I added a handler for the button that changes the state to the opposite of the current state.

    reOrderMoves() {
        this.setState({
            sortOrderDesc: (this.state.sortOrderDesc === true ? false : true),
        })
        this.render();
    }

Finally I made some changes to the Game->render() function.

    render() {
        
        // ....

        const sortOrderDesc = this.state.sortOrderDesc;
        const currentOrder = (sortOrderDesc ? '(Ascending)' : '(Descending)');
        
        
        const moves = history.map(...);
        
        if(sortOrderDesc === true) {
            moves.reverse();
        }

        // ....
}

This reverses the order of the moves array so it renders in reverse.

When someone wins, highlight the three squares that caused the win.

Ok I decided I need some way of identifying each square so I modified the Square component so that I can target the correct square.

function Square(props) {
    return (
        <button id={props.id} className={props.className} onClick={props.onClick}>
            {props.value}
        </button>
    )
}

Then I edited the Render Square function so the code sets the id and the className. I’m not sure which I’m going to use but this will give me a couple to try.

    renderSquare(i) {

        let className = 'square';
        
        if(this.props.winner) {
            let wRow = this.props.winner[1];
            for(let j = 0; j<wRow.length; j++) {
                if(wRow[j] == i) {
                    className = 'square winner';
                }
            }
        }
        return (
            <Square 
                key={i}
                id={i}
                className={className}
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
            />
        );
    }

(Later I settled on changing the class name if the number was in the winning row).

Then I edited the CalculateWinner function so it returns the winning row as well as the winner.

function calculateWinner(squares, rows, cols) {

    // ...

    for( let i=0; i< wLines.length; i++) {
        const [a,b,c] = wLines[i];
        if(squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return [squares[a], wLines[i]];
        }
    }
    return null;
}

Then I modified everywhere that Calculate Winner was referenced so it could still handle it properly.

    render() {
         // ....
         const winner = calculateWinner(current.squares, rows, cols);

         // ....

         if(winner) {
            status = 'Winner: ' + winner[0];
            
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }

        // ....

    }

Finally I added some CSS

.square.winner {
    background: green;
    color: #fff;
}

When no one wins, display a message about the result being a draw.

Last one 🙂

My solution was to check whether any of the squares were still null. If they weren’t and no winner had been selected it returned the draw result.

First a small modification to renderSquare to check the second array is not null.

renderSquare(i) {
        
        let className = 'square';
        
        if(this.props.winner && this.props.winner[1] !== null) {
            let wRow = this.props.winner[1];
            for(let j = 0; j<wRow.length; j++) {
                if(wRow[j] === i) {
                    className = 'square winner';
                }
            }
        }
        // ....
}

Then I modified calculateWinner function to include a bit below the winning line check.

function calculateWinner(squares, rows, cols) {
    
    //....

    let numberNotNull = 0;
    
    for( let j=0; j< squares.length; j++) {
        if(squares[j] !== null) {
            numberNotNull++;
        }
    }
    
    if(numberNotNull === (rows * cols)) {
        return ['Draw',null];
    }
    
    
    return null;
}

Having finished my game I ran the build function to create an optimised version.

$ yarn build
The Finished React Project
Learning to Use Node.js NPM and WebPack

Learning to Use Node.js NPM and WebPack

As part of my XD Plugin project I needed to use Node.js, npm and WebPack.js . I’m documenting this for my future reference.

Installing Node.js also installs a copy of npm. According to npm the copy of npm that Node.js installs is often out of date so I followed their instructions to install.

npm install npm@latest -g

Permissions Error

Note: If like me you get a permissions error just add sudo to the start. Only ever do this if you know what you are installing however!

Once that was installed I then installed Webpack.js using their instructions here : https://webpack.js.org/guides/installation/

Once I had finished that I went through the webpack basic tutorial in Terminal to get an idea what it was for.

The XD Plugin React tutorial I was following also uses Yarn so installed that as well. Yarn uses homebrew to install which I already have installed (it’s very useful).

The basic steps when creating a project that will be using a package library seems to be:

  1. Create a Project folder (mkdir)
  2. Navigate to above folder (cd)
  3. Create a package.json file (init) without asking questions ( -y )
  4. Install webpack locally and install webpack-cli this is used to run webpack on the command line
mkdir xdpluginreact
cd xdpluginreact
npm init -y
npm install webpack webpack-cli --save-dev

This creates the following files:

Screenshot of created folder structure

It is a good idea to seperate your plugin out into Dev and Distribution since we won’t need all the extra node_modules etc.

To create the initial plugin I followed the XD react tutorial: https://adobexdplatform.com/plugin-docs/tutorials/quick-start-react/

But when you finish that you end up with the main.js file located inside your development folder. You won’t want to copy the entire thing into Adobe XD because you don’t need all of it.

Adding a Distribution Folder

We want to end up with this instead:

A Screenshot of an example XD file

The example-plugin folder in the dist (distribution) folder is the one that gets copied to Adobe XD.

$ mkdir dist
$ mkdir dist/example-plugin
$ mv manifest.json dist/example-plugin
$ rm main.js

This creates the dist folder
Then Creates the example-plugin folder inside the dist folder
Moves the manifest.json (a file which Adobe XD needs) to dist/example-plugin
Deletes the currently compiled main.js

Then we need to change a couple of things. Open package.json and change the following line :

"main": "dist/example-plugin/main.js",

Open webpack.config.js and change the following line in the output section:

filename: "dist/example-plugin/main.js",
Screenshot of my webpack.config.js file.
This is what it should look like.

If I run yarn now this will create the main.js in the desired location

$ yarn build
WP wp-media.js – Bulk Upload

WP wp-media.js – Bulk Upload

Please Note:

This post is a work in progress and is mainly documentation on things I have discovered / learned while working on a project. There are probably better ways of doing some of this.

Some uploader settings

  • dropzone :
    This is the container element where the user will be dropping files. This triggers the drop files here when files are dragged over the element.
  • container :
    To confirm
  • browser :
    To confirm
  • error :
    Error handler e.g. function(e){ }
  • success :
    This fires when a file has been uploaded (it fires for each file that has been added).
  • added :
    This fires as each file is added to the drop zone (it fires for each file added).
  • progress :
    This fires while the file is being uploaded, (it fires for each file that has been added).
  • complete :
    I’ve never seen this fire so far so I’m not sure what triggers it.
  • refresh :
    I think this fires if the instance is refreshed but I haven’t confirmed.
uploader: {
	dropzone:  $('.dragzone'), // The dropzone container 
	container: $('.dragzone'),
	browser:   'browse_button',
	error: function(e) {	// if there is an error during upload this should fire. 

		console.log(e);
		console.log('error');

	},
	success: function(e) {	// once a file is uploaded this should fire. 

		//console.log(e);
		//console.log('success');
							
		fileTemplate(e);
							
		noItemsUp++; 	// update the number of items uploaded. 
		prevProgress = Math.floor(progressBar);
		progressBar = Math.floor(percent * noItemsUp);
							
		animateProgress();
							
		$('#importFiles').prop('disabled', false);
							
	},
	added: function(e) {	// this fires when a file is added. 
							
		noItems++; 	// add 1 to the number of items being uploaded
		percent		= 100 / noItems;	// work out the new percentage per item...
		prevProgress 	= progressBar;	// This is needed so the animated counter can work
		progressBar 	= percent * noItemsUp;		// This is the new progress the bar needs to move to. 
							
		animateProgress();
							
	},
	progress: function(e) {	// this fires while the file is being uploaded. 
		//console.log(e);
		//console.log('progress');
	},
	complete: function(e) { // ?
		//console.log(e);
		//console.log('complete');
	},
	refresh: function(e) { // ? 
		//console.log(e);
		//console.log('refresh');
	}
}
WP wp.media js – Add a File

WP wp.media js – Add a File

A work in progress blog post for information I’ve discovered so far about hooking into the WordPress Media Library model windows.

The javascript api in WordPress allows you to create custom upload fields and drag and drop zones in plugin and admin pages.

I’ve included some of my working code below – it is a work in progress currently.

Table of Contents

Adding a File Upload Field

The task : create a single file upload field on the add / edit page of a custom post type within a WordPress plugin. The file should use the WordPress media library functionality to upload and select the file.

I’ve added some screenshots of the finished field. I’ve added the field into the “Add New Custom Post Type” form using a meta box.

Screenshot of a meta box containing custom fields including a file upload field.

When the “Add the File” button is clicked (or the input field) the WordPress media library modal should appear.

I’ve modified the parameters to change the title of the modal window and the text on the button.

Once a file has been selected and chosen the information in the file preview box is filled out:

The Code

To use the WordPress Media api you need to include the media library scripts so that your custom script can access its functionality. To do this you will need to create a function for “enqueue”-ing scripts to your WordPress admin. Mine is created as part of a class for my plugin:

// Set some constants
defined( 'FMPLUGINURL' ) or define( 'FMPLUGINURL', plugin_dir_url( __FILE__ ) );

class FileManager {

	// Initialise the plugin
	public static function init() {
		add_action('admin_enqueue_scripts',['FileManager','queue_assets']);
	}

	// Add your assets to the admin view.
	public static function queue_assets() {

		// Checks that it's in the admin view
		if(is_admin()) {
			wp_enqueue_media(); // WP Media API
			wp_register_script( 'fm_media_uploader', FMPLUGINURL.'assets/js/fm_media_uploader.js');        // Customised WP media Upload Script for this plugin. 
			wp_localize_script( 'fm_media_uploader', 'FMPLUGINURL', FMPLUGINURL );
			wp_enqueue_script('fm_media_uploader');
		}
	}
}

FileManager::init();

WordPress References

wp_enqueue_media(); // WP Media

The above code adds all the assets necessary for using the media JavaScript API.

The fm_media_uploader file is where I am adding all the custom JavaScript to hook into the media API.

fm_media_uploader.js

First we need to setup an instance of the Media Library Modal which I will be storing in the variable below.

// Set some required variables 
var mediaUploader;

We also will need a trigger so that the Media Library Modal will open when certain elements are clicked.

// Set some required variables 
var mediaUploader;
		
// Set the triggers
$('#mediaUpload, .file-field input, .file-options a').on("click", function(e) {
			
	// This just prevents any default functions from occuring e.g. stops a form from submitting.
	e.preventDefault();

});

The instance of the media uploader will be created once the trigger is clicked.

// Set the media uploader with the following parameters 
mediaUploader = wp.media.frames.file_frame = wp.media({
	title: 'Choose File',		// Title at the top of the Modal Window
	button: {						// Button Parameters
		text: 'Choose File',	// Button Text
	}, 
	multiple: false 			// Allow multiple file uploads?
});

This sets up an instance of the Media Library Modal while modifying the title of the window and the button parameter. There is also an option to upload multiple or single files. There may be more parameters but these are the ones I know about so far. I may add to this post (and my next one) as I learn more.

This is how the modal window will look once triggered.

Finally we will tell the modal window to open:

// open the Media Uploader. 
mediaUploader.open();

So once the Add File button is clicked the WordPress Media Library modal window will appear and allow you to select a file. However you may find two problems:

  1. You can’t open the same modal window a second time.
  2. Nothing happens to the file once it is selected.

Prevent Multiple Instances

The reason you might have problems clicking the button a second time is that it is trying to create multiple instances on the same variable. So we need to add some code so the creation part is skipped if the instance has already been created.

if (mediaUploader) {
				
	// open the media uploader. 
	mediaUploader.open();
	return;
			
}      

If an instance of the Media Library Modal already exists it simply opens to existing instance and skips the part tht creates the instance.

I think this part could be cleaner. It is something I’m planning on revisiting later.

Handling the Selected File

The following piece of code handles the selected file and should be added when the instance of the Media Library Modal is created but before it is opened.

I may rewrite this bit in the future, as I think it could be neater.

// When a file is selected in the modal window....
mediaUploader.on('select', function() {
			
	var attachment = mediaUploader.state().get('selection').first().toJSON(); 	// the selected file is added to the selection list
	var fileDescription = $('textarea[name="FMDescription"]').val(); 		// get current File Description
	var file = attachment.filename.split('.');												// get the filename and split into an array. 
				
	file.reverse();		// reverse the array to make it easier to get the file extension
				
	$('.file-type').text('.'+file[0]);                                    		// File Extension
	$('input[name="FMFile"]').val(attachment.url);          // File URL
	$('.file-header').text(attachment.title);                         	// File Title
				
	if(fileDescription == '') { 
		$('textarea[name="FMDescription"]').val(attachment.description); 
	} // Set File Description if nothing has been filled in yet...
				
	$('.upload-date').text(attachment.dateFormatted);            // File Upload Date Formatted (this will use WordPress's default setting)
	$('.file-size').text(attachment.filesizeHumanReadable);      // File Size 
	$('.file-options .edit').attr('href', attachment.editLink).text('Edit'); 
	$('input[name="FM_attachment_id"]').val(attachment.id);     // Attachment (file) ID 
				
	var date = $('#jquery-datepicker').val();
				
	if(date) {
		$('input[name="FMTitle"]').val(attachment.title+' '+date);
	}
			
});

This may have more code than you need as I’ve taken it from my own project.

First we have an event listener that runs a function when the ‘select’ event is triggered on the Media Library Modal instance.

mediaUploader.on('select', function() { });

Then we need to collect all the details of the file that has been selected. This is returned in JSON format.

var attachment = mediaUploader.state().get('selection').first().toJSON();

Here’s an example of what it returns:

id: 58
title: "Example PDF Test"
filename: "Example-PDF.pdf"
url: "https://www.example.co.uk/wp-content/uploads/2019/12/Example-PDF.pdf"
link: "https://www.example.co.uk/?attachment_id=58"
alt: ""
author: "1"
description: "Example File Description"
caption: ""
name: "example-pdf"
status: "inherit"
uploadedTo: 0
date: Fri Dec 13 2019 16:13:24 GMT+0000 (Greenwich Mean Time) {}
modified: Thu Jan 30 2020 10:52:08 GMT+0000 (Greenwich Mean Time) {}
menuOrder: 0
mime: "application/pdf"
type: "application"
subtype: "pdf"
icon: "https://www.example.co.uk/wp-includes/images/media/document.png"
dateFormatted: "13th December 2019"
editLink: "https://www.example.co.uk/wp-admin/post.php?post=58&action=edit"
meta: false
authorName: "Rebecca Rumble"
filesizeInBytes: 23632
filesizeHumanReadable: "23 KB"
context: ""
compat: {item: "", meta: ""}

The rest of the code is just me telling the script to fill in various fields with the file data to display it like below:

Handling Edit Mode

When the custom post type is reopened in edit mode; it should automatically have the file selected when the file field is clicked. However at present the script won’t let this happen. We need to add some additional code to make this happen.

var fileID = $('input[name="FM_attachment_id"').val();

First we grab the attachment ID
(I use a hidden field to store this in the Meta Box).

Then in both the open existing mediaUploader statement and the mediaUploader.on(‘open’, function()) we need to add the following:

if(fileID) {
	// select the file ID to show it as selected in the Media Library Modal. 
	mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
	var selection = mediaUploader.state().get('selection');
	selection.add(wp.media.attachment(fileID));
}

It needs to be added twice because you might try and edit after you’ve added a file or when you are opening an existing custom post type. So it needs to check both in the creation of the instance and when an existing instance is opened.

I’m planning on working on this to try and make it neater and not repeat the same code twice but hey it works for now.

The Full Code

This is the current javaScript code in full…

// Set some required variables 
var mediaUploader;
		
// Set the triggers
$('#mediaUpload, .file-field input, .file-options a').on("click", function(e) {
			
	// This just prevents any default functions from occuring e.g. stops a form from submitting.
	e.preventDefault();
			
	// Fetch the value of the currently selected File - if nothing has been selected this will be empty
	var fileID = $('input[name="FM_attachment_id"').val();
			
	// If there is already a Media Library Modal only this block will run as it doesn't need to be initialised again. 
	if (mediaUploader) {
				
		// This should only be run if a file has already been selected
		if(fileID) {
					
			// when the media library modal is opened....
			mediaUploader.on('open', function() {
						
				// if there's a file ID
				if(fileID) {
					// select the file ID to show it as selected in the Media Library Modal. 
					mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
					var selection = mediaUploader.state().get('selection');
					selection.add(wp.media.attachment(fileID));
				}
			});
					
		}
				
		// open the media uploader. 
		mediaUploader.open();
		return;
			
	}      
			
	// Set the media uploader with the followign parameters 
	mediaUploader = wp.media.frames.file_frame = wp.media({
		title: 'Choose File',		// Title at the top of the Modal Window
		button: {						// Button Parameters
			text: 'Choose File',	// Button Text
		}, 
		multiple: false 			// Allow multiple file uploads?
	});
			
	// When the Media library Modal is opened... 
	mediaUploader.on('open', function() {
		// if there is a file ID...
		if(fileID) {
			// select the file ID to show it as selected in the Media Library Modal. 
			mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
			var selection = mediaUploader.state().get('selection');
			selection.add(wp.media.attachment(fileID));
		}
	});
		
	// When a file is selected in the modal window....
	mediaUploader.on('select', function() {
			
		var attachment = mediaUploader.state().get('selection').first().toJSON(); 	// the selected file is added to the selection list
		var fileDescription = $('textarea[name="FMDescription"]').val(); 		// get current File Description
		var file = attachment.filename.split('.');												// get the filename and split into an array. 
				
		file.reverse();		// reverse the array to make it easier to get the file extension
				
		$('.file-type').text('.'+file[0]);                                    		// File Extension
		$('input[name="FMFile"]').val(attachment.url);          // File URL
		$('.file-header').text(attachment.title);                         	// File Title
				
		if(fileDescription == '') { 
			$('textarea[name="FMDescription"]').val(attachment.description); 
		} // Set File Description if nothing has been filled in yet...
				
		$('.upload-date').text(attachment.dateFormatted);            // File Upload Date Formatted (this will use WordPress's default setting)
		$('.file-size').text(attachment.filesizeHumanReadable);      // File Size 
		$('.file-options .edit').attr('href', attachment.editLink).text('Edit'); 
		$('input[name="FM_attachment_id"]').val(attachment.id);     // Attachment (file) ID 
				
		var date = $('#jquery-datepicker').val();
				
		if(date) {
			$('input[name="FMTitle"]').val(attachment.title+' '+date);
		}
			
	});
			
	// open the Media Uploader. 
	mediaUploader.open();
			
});

Meta Box HTML

For reference this is the meta box HTML, it is also part of a PHP class.

<?php 
// display the contents of the metabox 
public static function html($post) {
        
	// https://developer.wordpress.org/reference/functions/get_terms/
	$terms      	= get_terms('file_category', array( 'hide_empty' => false ));
	$fileTerms  	= get_the_terms($post->ID,'file_category' );
	$fileCats   	= array();
            
	if(!empty($fileTerms)) {
		foreach($fileTerms as $fileTerm) {
			$fileCats[] = $fileTerm->term_id;
		}
	}
            
	$FMDate			= get_post_meta($post->ID,'FMDate',true);
	$dateObj		= datetime::createfromformat('Ymd',$FMDate);
	$FM_attachment_id	= get_post_meta($post->ID,'FM_attachment_id',true);
	$attachment		= array();
            
	if(!empty($FM_attachment_id)) {
            
		// https://developer.wordpress.org/reference/functions/wp_prepare_attachment_for_js/ 
		$attachment  = wp_prepare_attachment_for_js($FM_attachment_id);
		$fileExt     = explode('.',$attachment['filename']);
		$fileExt     = array_reverse($fileExt);
               
	}
?>
	<div class="adminform">
		<div class="row">
			<div class="m-all t-all d-all">
				<label for="FMTitle">
					<span class="label">Title: </span>
					<input type="text" name="FMTitle" value="<?php echo (!empty($post->post_title) ? $post->post_title : 'This will be automatically created once details have been entered'); ?>" readonly />
				</label>
			</div>
		</div>
		<div class="row">
			<div class="m-all t-all d-all file_field">
				<label for="FMFile">
					<span class="label">File: </span>
					<input type="hidden" name="FM_attachment_id" value="<?php echo (int) $FM_attachment_id; ?>">
					<input type="text" name="FMFile" value="<?php echo (!empty($attachment) ? $attachment['url'] : ''); ?>" placeholder="Click 'Add the File' to select or upload file" required />
				</label>
				<input type="button" id="mediaUpload" class="button-primary" value="Add the File" />
			</div>
		</div>
		<div class="row">
			<div class="m-all t-all d-all file_information">
				<div class="spacer"></div>
				<div class="file_wrapper">
					<div class="file-icon">
						<img src="<?php echo FMPLUGINURL.'/assets/img/046.svg'; ?>" alt="File icon" class="icon-color" />
						<span class="file-type"><?php echo (!empty($attachment) ? '.'.$fileExt[0] : 'none'); ?></span>
					</div>
					<div class="file-content">
						<div class="file-header"><?php echo (!empty($attachment) ? $attachment['title'] : ''); ?></div>
						<div class="upload-date"><?php echo (!empty($attachment) ? $attachment['dateFormatted'] : ''); ?></div>
						<div class="file-size"><?php echo (!empty($attachment) ? $attachment['filesizeHumanReadable'] : ''); ?></div>
						<div class="file-options"><?php echo (!empty($attachment) ? '<a href="'.$attachment['editLink'].'">Edit</a>' : '<a href="" class="edit"></a>'); ?></div>
					</div>
				</div>
			</div>
		</div>
		<div class="row flowwrap">
			<div class="m-all t-1of2 d-1of2">
				<label for="FMCat">
					<span class="label">File Category: </span>
					<select name="FMCat">
						<option>Select a File Category</option>
						<?php 
						if(!empty($terms)) {
							foreach($terms as $term) {
								echo '<option value="'.$term->term_id.'"'.(in_array($term->term_id,$fileCats) ? ' selected' : '').'>'.$term->name.'</option>';
							}
						} ?>
					<select>
                            
				</label>
			</div>
			<div class="m-all t-1of2 d-1of2">
				<label for="FMDate">
					<span class="label">Date: </span>
					<input type="text" id="jquery-datepicker" name="FMDate" value="<?php echo (!empty($dateObj) ? $dateObj->format('d/m/Y') : ''); ?>" placeholder="Click to add date" autocomplete="off" required />
				</label>
			</div>
		</div>
		<div class="row">
			<div class="m-all t-all d-all">
				<label for="FMDescription">
					<span class="label">Description: </span>
					<br />
					<textarea name="FMDescription"><?php echo $post->post_content; ?></textarea>
				</label>
			</div>
		</div>
	</div>
	<?php
            
}
?>

“Sorry. You cannot attach Files to this post”

I came across this odd bug. This only occurs if you try to upload a file before selecting a file. However if you select a file, then go back and try and upload a file it then works?

I spent a lot of time hunting for the solution but in the end I discovered a tutorial on wp.media which gave me a clue. https://code.tutsplus.com/series/getting-started-with-the-wordpress-media-uploader–cms-666

If I added the following to the mediaUploader it worked as it should, however this had the effect of losing the title option. I believe the title and button options only work on a custom frame.

mediaUploader = wp.media.frames.file_frame = wp.media({
    frame:    'post',
    state:    'insert',
    title: 'Choose File',
    button: {
        text: 'Choose File',
    },
    multiple:false,
});

I also had to change the select listener to ‘insert’

// When a file is selected in the modal window....
// mediaUploader.on('select', function() {
mediaUploader.on('insert', function() {

To be continued -> Bulk Upload via wp.media

Useful References

WordPress Codex wp.media

https://github.com/ericandrewlewis/wp-media-javascript-guide

https://wordpress.org/support/article/media-library-screen/

https://code.tutsplus.com/series/getting-started-with-the-wordpress-media-uploader–cms-666 – One of the best tutorials for Wp.media

CSS Pointer Events

CSS Pointer Events

This is maybe only needed in some edge cases, but you can use pointer-events to make psuedo elements (:after and :before) clickable via javascript.

Note: Preferable to use clickable elements if you can though!

My original code when I needed this as a work around.

SCSS Code

.add-chevron {
    // prevents clicking on element from working
    pointer-events: none; 

    // Necessary for allowing any clickable elements within the parent to work. 
    a, a:visited, button {
        pointer-events: all;
    }
    &:after {
        content: 'click me';
        display: block;
        height: 34px;
        width: 34px;
        // allows this pseudo element to be clickable
        pointer-events: all;
    }
}
CSS Tricks : pointer-events

https://caniuse.com/#feat=pointer-events

Search for string in files

Search for string in files

$ grep -r -i "string" /your/directory/path/

-r (search recursively)

-i (case insensitive)

-l (list only file names)

--include=\*.${file_extension} – search files that match the extension(s) or file pattern only

grep -r -i -l --include \*.jpg --include \*.png --include \*.gif "Mouse" /your/directory/path/