【PostCSS】【TypeScript】Vertical Selection
Tags: typescript, postcss
Published: 2020-10-25 14:02:03

## Intro
For example, when I use table elements, I only can select horizontally.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/90fmbsmm6c3ds6vl9q3w.jpg)

#### Index.html
```html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" type="text/css" href="../css/home_page.css" >
    </head>
    <body>
        <div>First element</div>
        <table id="table_area">
            <tr class="table_row">
                <td class="table_cell">1.Hello</td>
                <td class="table_cell">1.World</td>
                <td class="table_cell"></td>
                <td class="table_cell"></td>
                <td class="table_cell"></td>
                <td class="table_cell">1.!!!</td>
            </tr>
            <tr class="table_row">
                <td class="table_cell">2.Hello</td>
                <td class="table_cell">2.World</td>
                <td class="table_cell"></td>
                <td class="table_cell"></td>
                <td class="table_cell"></td>
                <td class="table_cell">2.!!!</td>
            </tr>
        </table>
    </body>
</html>
```
#### home_page.css
```css
#table_area{
    margin: 1%;
    width: 10vw;
    height: 5vh;
    border: 1px solid black;
    border-collapse: collapse;
}
.table_cell{
    border: 1px solid black;
    border-collapse: collapse;
    min-width: 2vw;
}
```

But in this time, I want to select each column.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/biroioie7u5szaask88j.jpg)

### Environments
* Node.js ver.15.0.0

### package.json
```json
{
    "browserslist": [
        "last 2 version"
    ],
    "scripts": {
        "css": "npx postcss postcss -c postcss.config.js -d wwwroot/css -w"
    },
    "dependencies": {
        "autoprefixer": "^10.0.1",
        "postcss": "^8.1.2",
        "postcss-cli": "^8.1.0",
        "postcss-import": "^13.0.0",
        "precss": "^4.0.0",
        "ts-loader": "^8.0.6",
        "tsc": "^1.20150623.0",
        "typescript": "^4.0.3",
        "webpack": "^5.2.0",
        "webpack-cli": "^4.1.0"
    }
}
```
## [PostCSS] Input Error: You must pass a valid list of files to parse
On my Surface 6, when I tried executing compiling PostCSS command "npx postcss postcss/*.css -c postcss.config.js -d wwwroot/css", I got an error.

```
Input Error: You must pass a valid list of files to parse
```

But on my another PC didn't occurr.
Though I haven't know why, I could avoid this error by changing the command to "npx postcss <b>postcss</b> -c postcss.config.js -d wwwroot/css"
("postcss" was a directory name).

## Arrange elements vertically
Because I want to make elements select vertically, I have to arrange them vertically.
This time, I use Flexbox. 

#### Index.html
```html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" type="text/css" href="../css/home_page.css" >
    </head>
    <body>
...
        <div id="item_area">
            <div class="item_column">
                <div class="item_cell">1. hello</div>
                <div class="item_cell">1. world</div>
                <div class="item_cell"></div>
                <div class="item_cell"></div>
                <div class="item_cell"></div>
                <div class="item_cell">1. !!!</div>
            </div>
            <div class="item_column">
                <div class="item_cell">2. hello</div>
                <div class="item_cell"></div>
                <div class="item_cell"></div>
                <div class="item_cell"></div>
                <div class="item_cell">2. world</div>
                <div class="item_cell">2. !!!</div>
            </div>            
        </div>
    </body>
</html>
```
#### home_page.css
```css
...
#item_area{
    display: flex;
    flex-direction: row;
}
.item_column{
    display: flex;
    flex-direction: column;
}
.item_cell{
    border: 1px solid black;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 10vw;
    height: 5vh;
}
```
To compile PostCSS, I execute "npm run css" or "npx postcss postcss -c postcss.config.js -d wwwroot/css".
Now I can select vertically.

## Copy & paste
One problem is occurred when I try copying the elements and pasting another place(For example on an Excel sheet).
The copied values are like below.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0ahmchkk4mwoqwbbtem8.jpg)

The empty values and columns are ignored.
Now I try to edit copied values by TypeScript.

#### tsconfig.json
```json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["DOM", "ES2015"],
    "outDir": "./wwwroot/js",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
```
#### webpack.config.js
```js
var path = require('path');
module.exports = {
    mode: 'development',
    entry: {
        'homePage': './ts/home.page.ts',
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, './wwwroot/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};
```
#### Index.html
```
...
        <script src="../js/homePage.bundle.js"></script>
        <script>
            (function() {
                Page.init();
            })();
        </script>
    </body>
</html>
```
#### home.page.ts
```ts
import { CopyCellValues } from "./copy-cell-values";

var ctrlActive: boolean = false;
export function init()
{
    // track Ctrl + C
    document.body.addEventListener('keydown', e => handleKeyDownEvents(e));
    document.body.addEventListener('keyup', e => handleKeyUpEvents(e));
}
function handleKeyDownEvents(e: KeyboardEvent)
{
    switch(e.key)
    {
        case 'Control':
            ctrlActive = true;
            break;
    }
}
function handleKeyUpEvents(e: KeyboardEvent)
{
    switch(e.key)
    {
        case 'c':
            copyToClipboard();
            break;
        case 'Control':
            ctrlActive = false;
            break;
    }
}
function copyToClipboard()
{
    if (ctrlActive === false)
    {
        return;
    }
    const copyCellValues = new CopyCellValues();
    copyCellValues.copyToClipboard();
}
```
#### copy-cell-values.ts
```ts
export class CopyCellValues {
    public copyToClipboard() {
        const selection = window.getSelection();
    
        if(selection == null ||
            selection.rangeCount <= 0) {
            console.warn("no selection");
            return;
        }
        const targetTexts = this.getSelectedTexts(selection.getRangeAt(0).cloneContents());
        this.copy(targetTexts);
    }
    private getSelectedTexts(selectedDom: DocumentFragment): Array<Array<string>> {
        let parent: HTMLElement|null = null;
        for(let i = 0; i < selectedDom.childNodes.length; i++) {
            const targetElement = selectedDom.childNodes[i] as HTMLElement;
            if (targetElement?.id === 'item_area') {
                parent = targetElement;
                break;
            }
        }
        if(parent == null) {
            // when select inside "item_area"
            return this.getTexts(selectedDom.childNodes);
        }
        // when select outside "item_area"
        return this.getTexts(parent.childNodes);
    }
    private getTexts(parentNodes: NodeListOf<ChildNode>): Array<Array<string>> {
        let results = new Array<Array<string>>();
        for(let i = 0; i < parentNodes.length; i++) {
            const targetElement = parentNodes[i] as HTMLElement;
            if (targetElement.className !== 'item_column') {
                continue;
            }
            let gotTexts: Array<string>| null = null;
            for(let j = 0; j < targetElement.childNodes.length; j++) {
                let child = targetElement.childNodes[j] as HTMLElement;
                if (child.className !== 'item_cell' ||
                        child.textContent == null) {
                    continue;
                }
                if (gotTexts == null) {
                    gotTexts = new Array<string>();
                }
                gotTexts.push(child.textContent);
            }
            if(gotTexts == null) {
                continue;
            }
            results.push(gotTexts);
        }
        return this.addEmptyRows(results, this.getMaxRowCount(results));
    }
    private getMaxRowCount(texts: Array<Array<string>>): number {
        let result = 0;
        for(const columnItems of texts) {
            const rowCount = columnItems.length;
            if (result < rowCount) {
                result = rowCount;
            }
        }
        return result;
    }
    private addEmptyRows(texts: Array<Array<string>>, rowCount: number): Array<Array<string>> {
        if(texts.length <= 0) {
            return texts;
        }
        if (texts[0].length < rowCount) {
            const addedFirstTexts = new Array<string>();
            const addEmptyCount = rowCount - texts[0].length;
            for(let i = 0; i < addEmptyCount; i++) {
                addedFirstTexts.push('');
            }
            for(const t of texts[0]) {
                addedFirstTexts.push(t);
            }
            texts[0] = addedFirstTexts;
        }
        if(texts.length > 1) {
            const targetIndex = texts.length - 1;
            if (texts[targetIndex].length < rowCount) {
                const addedLastTexts = new Array<string>();
                const addEmptyCount = rowCount - texts[targetIndex].length;
                for(const t of texts[0]) {
                    addedLastTexts.push(t);
                }
                for(let i = 0; i < addEmptyCount; i++) {
                    addedLastTexts.push('');
                }
                texts[targetIndex] = addedLastTexts;
            }
        }
        return texts;
    }
    private copy(texts: Array<Array<string>>) {
        let result = '';
        const rowCount = texts[0].length;
        for(let i = 0; i < rowCount; i++) {
            let rowText = '';
            for(const columnItem of texts) {
                if (rowText !== '') {
                    rowText += '\t';
                }
                rowText += columnItem[i];
            }
            result += rowText;
            result += '\n';
        }
        navigator.clipboard.writeText(result);
    }
}
```
The result like below.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wxbmay6zquyam7ocdfm2.jpg)

I can get selected elements by "getSelection".

* [Window.getSelection() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection)

And I can rewrite the clipboard values by "navigator.clipboard.writeText".

* [Clipboard.writeText() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText)

## Editable elements
Now I try making the elements editable.
I can use "contentEditable" to do this.

* [Making content editable - Developer guides | MDN](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content)

But when I make the elements editable, I get another problem.
When I focus the editable element, I can't select other elements.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zioiz988gaqcbpouk8s0.jpg)

So I have to distinguish edit and select operations.

I decide when I double click an element, the editing mode is started.
After that, if I click other elements, the editing mode is stopped, and the selecting mode is started.

Maybe I can write more simpler codes with jQuery plugin or RxJs or other libraries.
But in this time, I try using only TypeScript.

#### home.page.ts
```ts
import { ClickEventHandler } from "./click-event-handler";
import { CopyCellValues } from "./copy-cell-values";

var ctrlActive: boolean = false;
export function init()
{
...
    const clickEvent = new ClickEventHandler();
    clickEvent.init();
}
...
```
#### click-event-handler.ts
```ts
export class ClickEventHandler {
    private clickedElement: HTMLElement|null = null;
    private lastClickeTime: number = 0;
    private editing: boolean = false;
    public init() {
        document.onclick = e => this.handleClickEvent(e);
    }
    private handleClickEvent(e: MouseEvent){
        const target = e.target as HTMLElement;
        if(target.className !== 'item_cell') {
            if (this.clickedElement != null) {
                this.clickedElement.contentEditable = 'false';
            }
            this.clickedElement = null;
            this.editing = false;
            this.lastClickeTime = 0;
            return;
        }
        const clickedTime = (new Date()).getTime();
        if (this.editing === false &&
            this.clickedElement != null) {    
            if ((clickedTime - this.lastClickeTime) > 1000) {
                this.clickedElement.contentEditable = 'false';
                this.clickedElement = null;
            }
        }
        this.lastClickeTime = clickedTime;
        if (target === this.clickedElement) {
            target.contentEditable = 'true';
            target.focus();
            this.editing = true;
            return;
        } else {
            if (this.clickedElement != null) {
                this.clickedElement.contentEditable = 'false';
            }
            target.contentEditable = 'false';
            this.editing = false;
        }
        this.clickedElement = e.target as HTMLElement;    
    }
}
```