├── .gitignore
├── .prettierrc
├── .DS_Store
├── logo.png
├── src
├── tailwind.css
├── utils.ts
├── handleClosePopup.ts
├── insertUUID.ts
├── insertTemplatedBlock.ts
├── searchbar.tsx
├── App.tsx
├── parser.tsx
├── index.tsx
├── inserterUI.tsx
└── App.css
├── .github
├── .DS_Store
└── workflows
│ └── publish.yml
├── .postcssrc
├── tailwind.config.js
├── index.html
├── tsconfig.json
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .parcel-cache
3 | dist
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false
4 | }
5 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/.DS_Store
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/logo.png
--------------------------------------------------------------------------------
/src/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/.github/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sawhney17/logseq-smartblocks/HEAD/.github/.DS_Store
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "postcss-import": {},
4 | "tailwindcss/nesting": {},
5 | "tailwindcss": {},
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/handleClosePopup.ts:
--------------------------------------------------------------------------------
1 | export const handleClosePopup = () => {
2 | // ESC
3 | document.addEventListener(
4 | 'keydown',
5 | function (e) {
6 | if (e.keyCode === 27) {
7 | logseq.hideMainUI({ restoreEditingCursor: true });
8 | }
9 | e.stopPropagation();
10 | },
11 | false
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{vue,js,ts,jsx,tsx,hbs,html}'],
3 | darkMode: 'media', // or 'media' or 'class'
4 | theme: {
5 | extend: {
6 | spacing: {
7 | 100: '50rem',
8 | },
9 | },
10 | },
11 | variants: {
12 | extend: {},
13 | },
14 | plugins: [],
15 | };
16 |
--------------------------------------------------------------------------------
/src/insertUUID.ts:
--------------------------------------------------------------------------------
1 | import '@logseq/libs'
2 |
3 | export function persistUUID(uuid) {
4 | logseq.Editor.getBlock(uuid, { includeChildren: true }).then(
5 | (result) => {
6 | if (!result.content.match(/(id:: .*)/g)){
7 | const finalContent = result.content + `\nid:: ${result.uuid}`
8 | logseq.Editor.updateBlock(result.uuid, finalContent)
9 | }
10 | })
11 |
12 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | Document
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": false,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": false,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "noImplicitAny": false,
18 | "jsx": "react-jsx"
19 | },
20 | "include": ["./src"]
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Aryan Sawhney
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq-smartblocks-plugin",
3 | "version": "3.5.1",
4 | "description": "A plugin to replicate Smartblocks",
5 | "author": "@sawhney17",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "parcel ./index.html --public-url ./",
9 | "build": "parcel build --public-url . --no-source-maps index.html"
10 | },
11 | "devDependencies": {
12 | "@logseq/libs": "^0.0.6",
13 | "@types/react": "^17.0.38",
14 | "@types/react-dom": "^17.0.11",
15 | "autoprefixer": "^10.4.2",
16 | "parcel": "^2.0.0",
17 | "postcss": "^8.1.0"
18 | },
19 | "dependencies": {
20 | "@emotion/react": "^11.9.0",
21 | "@emotion/styled": "^11.8.1",
22 | "@mui/material": "latest",
23 | "autoprefix": "^1.0.1",
24 | "axios": "^0.26.1",
25 | "logseq-dateutils": "latest",
26 | "postcss-cli": "^9.1.0",
27 | "postcss-import": "^14.0.2",
28 | "react": "^17.0.2",
29 | "react-dom": "^17.0.2",
30 | "react-tooltip": "^4.2.21",
31 | "sherlockjs": "^1.4.2",
32 | "tailwindcss": "^3.0.15"
33 | },
34 | "logseq": {
35 | "id": "logseq-smartblocks",
36 | "main": "dist/index.html",
37 | "icon": "./logo.png"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build plugin
2 |
3 | on:
4 | push:
5 | # Sequence of patterns matched against refs/tags
6 | tags:
7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
8 |
9 | env:
10 | PLUGIN_NAME: logseq-templater-plugin
11 | APIKEY: ${{ secrets.APIKEY }}
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: '16.x' # You might need to adjust this value to your own version
23 | - name: Build
24 | id: build
25 | env:
26 | APIKEY: ${{ secrets.APIKEY }}
27 | run: |
28 | npm i && npm run build
29 | mkdir ${{ env.PLUGIN_NAME }}
30 | cp README.md package.json logo.png ${{ env.PLUGIN_NAME }}
31 | mv dist ${{ env.PLUGIN_NAME }}
32 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
33 | ls
34 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
35 | - name: Create Release
36 | uses: ncipollo/release-action@v1
37 | id: create_release
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | VERSION: ${{ github.ref }}
41 | with:
42 | allowUpdates: true
43 | draft: false
44 | prerelease: false
45 |
46 | - name: Upload zip file
47 | id: upload_zip
48 | uses: actions/upload-release-asset@v1
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | with:
52 | upload_url: ${{ steps.create_release.outputs.upload_url }}
53 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
54 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
55 | asset_content_type: application/zip
56 |
57 | - name: Upload package.json
58 | id: upload_metadata
59 | uses: actions/upload-release-asset@v1
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 | with:
63 | upload_url: ${{ steps.create_release.outputs.upload_url }}
64 | asset_path: ./package.json
65 | asset_name: package.json
66 | asset_content_type: application/json
67 |
--------------------------------------------------------------------------------
/src/insertTemplatedBlock.ts:
--------------------------------------------------------------------------------
1 | import '@logseq/libs'
2 | import {
3 | BlockEntity,
4 | IBatchBlock,
5 | } from '@logseq/libs/dist/LSPlugin.user';
6 |
7 | import { parseDynamically } from './parser';
8 |
9 | import { renderApp, valueArray } from './index';
10 | export var networkRequest = false
11 | export var stopStatus = true
12 | export function editNetworkRequest(value) {
13 | networkRequest = value
14 | }
15 |
16 | export var data = null
17 | // const reg = /<%([^%].*?)%>/g
18 | const reg = /<%(.*?)%>/g
19 | export var blockUuid2
20 | export var sibling
21 | var currentRun = 1
22 | var previousRun = 0
23 | async function triggerParse(obj) {
24 | if (obj.content) {
25 | let regexMatched = obj.content.match(reg)
26 | for (const x in regexMatched) {
27 | let toBeParsed = obj.content
28 | var currentMatch = regexMatched[x]
29 | let formattedMatch = await parseDynamically(currentMatch);
30 | let newRegexString = toBeParsed.replace(currentMatch, formattedMatch)
31 | obj.content = newRegexString
32 | obj.properties = {}
33 | }
34 | }
35 | currentRun += 1
36 | await obj.children.map(triggerParse)
37 | }
38 |
39 | export function triggerParseInitially(obj) {
40 | console.log(obj)
41 | if (obj.content) {
42 |
43 | let regexMatched = obj.content.match(reg)
44 | // delete obj.uuid
45 | // delete obj.id
46 | // delete obj.left
47 | // delete obj.parent
48 | // delete obj.pathRefs
49 | for (const x in regexMatched) {
50 | var currentMatch = regexMatched[x]
51 | if (currentMatch.toLowerCase().includes("setinput:")) {
52 | const inputs = currentMatch.slice(2, -2).split(":")
53 | const variableName = inputs[1]
54 | const variableOptions = inputs[2]?.split(",")
55 | variableOptions ? valueArray.push({ value: "", name: variableName, options: variableOptions }) : valueArray.push({ value: "", name: variableName })
56 | }
57 | }
58 | }
59 | obj.children.map(triggerParseInitially)
60 | }
61 |
62 | export async function insertProperlyTemplatedBlock(blockUuid3, template2, sibling3) {
63 | var query = `
64 | [:find (pull ?b [*])
65 | :where
66 | [?b :block/properties ?p]
67 | [(get ?p :template) ?ty]
68 | [(= "${template2}" ?ty)]]`
69 | blockUuid2 = blockUuid3
70 | sibling = sibling3
71 | let refUUID
72 | try {
73 | let ret = await logseq.DB.datascriptQuery(query)
74 | const results = ret?.flat()
75 | if (results && results.length > 0) {
76 | refUUID = results[0].uuid
77 | console.log(refUUID)
78 | console.log(results)
79 | let origBlock = await logseq.Editor.getBlock(refUUID, {
80 | includeChildren: true,
81 | })
82 | data = origBlock
83 | console.log("origBlock")
84 | console.log(origBlock)
85 | triggerParseInitially(origBlock)
86 | if (valueArray.length > 0) {
87 | renderApp()
88 | logseq.showMainUI()
89 | }
90 | else {
91 | logseq.hideMainUI({ restoreEditingCursor: true });
92 | insertProperlyTemplatedBlock2(blockUuid3, sibling3, origBlock)
93 | }
94 |
95 | }
96 | } catch (error) {
97 | }
98 | }
99 | export async function insertProperlyTemplatedBlock2(blockUuid, sibling2, origBlock) {
100 | data = origBlock
101 | async function insertFinally() {
102 | let page = await logseq.Editor.getPage(blockUuid)
103 | if (page != undefined) {
104 | console.log(
105 | "iserting"
106 | )
107 | let blockTree = (await logseq.Editor.getPageBlocksTree(blockUuid))
108 | let lastBlock = blockTree[blockTree.length - 1]
109 | logseq.Editor.insertBatchBlock(lastBlock.uuid, data.children as unknown as IBatchBlock, { sibling: true })
110 | }
111 | else {
112 | logseq.Editor.insertBatchBlock(blockUuid, data.children as unknown as IBatchBlock, { sibling: (sibling2 === 'true') })
113 | }
114 |
115 | }
116 |
117 | triggerParse(data)
118 | timeOutShouldBeSet()
119 | function checkDiff() {
120 |
121 | if (currentRun != previousRun) {
122 | previousRun = currentRun
123 | timeOutShouldBeSet()
124 | }
125 | else {
126 | if (networkRequest == true) {
127 | setTimeout(function () {
128 | checkDiff()
129 | networkRequest = false
130 | }, 500);
131 | }
132 | else {
133 | logseq.App.showMsg("Run has begun")
134 | insertFinally()
135 | }
136 | }
137 | }
138 | function timeOutShouldBeSet() {
139 | setTimeout(function () {
140 | checkDiff()
141 | }, 100);
142 | }
143 | if (origBlock.children.length === 0 || !origBlock.children) {
144 | logseq.App.showMsg("Whoops! Doesn't look like there's any content under the template.");
145 | }
146 | }
147 | // }
148 | // catch (err) {
149 | // console.log(err)
150 | // }
151 | // }
--------------------------------------------------------------------------------
/src/searchbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import ReactDOM from "react-dom";
3 | import "./App.css";
4 | import "@logseq/libs";
5 | import "./tailwind.css";
6 | import { insertProperlyTemplatedBlock } from "./insertTemplatedBlock";
7 |
8 | export let smartblocks;
9 |
10 | export async function updateTemplates() {
11 | let query = `
12 | [:find (pull ?b [*])
13 | :where
14 | [?b :block/properties ?p]
15 | [(get ?p :template)]]
16 | `;
17 | let result = await logseq.DB.datascriptQuery(query)
18 | smartblocks = result.map((item) => item[0].properties.template);
19 | return result.map((item) => item[0].properties.template);
20 | }
21 | const SearchBar: React.FC<{ blockID }> = ({ blockID }) => {
22 | const [searchTerm, setSearchTerm] = React.useState("");
23 | const [searchResults, setSearchResults] = React.useState([]);
24 | const [highlightedResult, setHighlightedResult] = React.useState(null);
25 | const firstUpdate = useRef(true);
26 |
27 | React.useEffect(() => {
28 | setTimeout(() => {
29 | const input = document.getElementById("smartblockSearchbar");
30 | if (input) {
31 | input.focus();
32 | }
33 | }, 100);
34 | }, []);
35 | const handleChange = (event) => {
36 | setSearchTerm(event.target.value);
37 | };
38 | React.useEffect(() => {
39 | let results;
40 | updateTemplates();
41 | if (searchTerm != "") {
42 | results = smartblocks.filter((template) =>
43 | template.toLowerCase().includes(searchTerm)
44 | );
45 | } else {
46 | results = smartblocks;
47 | }
48 | setSearchResults(results);
49 | }, [searchTerm]);
50 | React.useEffect(() => {
51 | if (firstUpdate.current) {
52 | firstUpdate.current = false;
53 | return;
54 | }
55 |
56 | setHighlightedResult(0);
57 | }, [searchResults]);
58 |
59 | React.useEffect(() => {
60 | if (firstUpdate.current) {
61 | return;
62 | }
63 | document.addEventListener("keydown", keyControl);
64 | updateHighlight();
65 | return () => document.removeEventListener("keydown", keyControl);
66 | }, [highlightedResult]);
67 | function keyControl(e) {
68 | if (e.keyCode === 40) {
69 | //down arrow
70 | if (highlightedResult < searchResults.length - 1) {
71 | let hello = highlightedResult + 1;
72 | setHighlightedResult(hello);
73 | } else {
74 | setHighlightedResult(0);
75 | }
76 | }
77 | if (e.keyCode === 38) {
78 | //Up arrow
79 | if (highlightedResult > 0) {
80 | setHighlightedResult(highlightedResult - 1);
81 | } else {
82 | setHighlightedResult(searchResults.length - 1);
83 | }
84 | }
85 | if (e.keyCode === 13) {
86 | //EnterKey arrow
87 | insertProperlyTemplatedBlock(
88 | blockID,
89 | searchResults[highlightedResult],
90 | "true"
91 | );
92 | }
93 | e.handled = true;
94 | }
95 | const insertBlocks = (e) => {
96 | insertProperlyTemplatedBlock(blockID, e.target.id, "true");
97 | };
98 | const updateHighlight = () => {
99 | for (const x in searchResults) {
100 | if (x == highlightedResult) {
101 | document.getElementById(searchResults[x]).classList.add("bg-[#4c4c4c]");
102 | } else {
103 | document
104 | .getElementById(searchResults[x])
105 | .classList.remove("bg-[#4c4c4c]");
106 | }
107 | }
108 | };
109 |
110 | return (
111 |
112 |
113 |
114 |
122 |
123 | {searchResults.map((item) => (
124 |
129 |
134 | T
135 |
136 |
-
140 | {item}
141 |
142 |
143 | ))}
144 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | export default SearchBar;
152 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { getDateForPage } from "logseq-dateutils";
3 | import { valueArray, valueZero, editValueArray } from "./index";
4 | import "./App.css";
5 | import {
6 | data,
7 | insertProperlyTemplatedBlock2,
8 | blockUuid2,
9 | sibling,
10 | } from "./insertTemplatedBlock";
11 | import { Autocomplete } from "@mui/material";
12 |
13 | var replacementArray = {};
14 | function triggerParse(obj) {
15 | obj.properties = {};
16 | const reg = /<%setInput:([^%].*?)%>/gi;
17 | const reg2 = /<%getInput:([^%].*?)%>/gi;
18 | if (obj.content) {
19 | var newRegexString = obj.content;
20 | const regexMatched = obj.content.match(reg);
21 | for (const x in regexMatched) {
22 | const toBeParsed = newRegexString;
23 | var currentMatch = regexMatched[x];
24 | const variableName = currentMatch.slice(2, -2).split(":")[1];
25 | newRegexString = toBeParsed.replace(
26 | currentMatch,
27 | replacementArray[variableName]
28 | );
29 | }
30 | const regexMatched2 = obj.content.match(reg2);
31 | for (const x in regexMatched2) {
32 | const toBeParsed = newRegexString;
33 | var currentMatch = regexMatched2[x];
34 | const variableName = currentMatch.slice(2, -2).split(":")[1];
35 | newRegexString = toBeParsed.replace(
36 | currentMatch,
37 | replacementArray[variableName]
38 | );
39 | }
40 | obj.content = newRegexString;
41 | }
42 | obj.children.map(triggerParse);
43 | }
44 |
45 | export interface FormValues {
46 | name: string
47 | value: string
48 | options?: string[]
49 | }
50 | const App = () => {
51 | //Add type annotation to the state
52 | const [formValues, setFormValues] = useState([]);
53 | const [isOpened, setIsOpened] = useState(true);
54 | const [isSubmitted, setIsSubmitted] = useState(false);
55 | const [something, setSomething] = useState(false);
56 |
57 | //force remount the component upon formValues changing
58 | React.useEffect(() => {
59 | setFormValues(formValues);
60 | console.log(something)
61 | }
62 | , [formValues]);
63 | let resetExit = (event) => {
64 | event.preventDefault();
65 | setIsSubmitted(false);
66 | setIsOpened(true);
67 | setFormValues([]);
68 | valueZero();
69 | logseq.hideMainUI({ restoreEditingCursor: true });
70 | };
71 | let updateUI2 = () => {
72 | let newFormValues = [...formValues];
73 | for (const x in valueArray) {
74 | console.log(valueArray)
75 | newFormValues.push(valueArray[x]);
76 | }
77 | console.log(formValues)
78 | setFormValues(newFormValues);
79 | setIsSubmitted(true);
80 | setIsOpened((wasOpened) => !wasOpened);
81 | };
82 | let handleChange = (i, e) => {
83 | let newFormValues = [...formValues];
84 | newFormValues[i]["value"] = e.target.value;
85 | setFormValues(newFormValues);
86 | };
87 | let handleChange2 = (i, e) => {
88 | let newFormValues = [...formValues];
89 | newFormValues[i]["value"] = e.target.value;
90 | setFormValues(newFormValues);
91 | }
92 |
93 | let handleSubmit = (event) => {
94 | event.preventDefault();
95 | setIsSubmitted(false);
96 | setIsOpened(true);
97 | setFormValues([]);
98 | editValueArray(formValues);
99 | logseq.hideMainUI({ restoreEditingCursor: true });
100 | for (const x in valueArray) {
101 | //if value is empty and there exists options, use the first option instead
102 | if (valueArray[x].value === "" && valueArray[x].options) {
103 | valueArray[x].value = valueArray[x].options[0];
104 | }
105 | const value = valueArray[x].value;
106 | const name = valueArray[x].name;
107 | replacementArray[name] = value;
108 | }
109 | triggerParse(data);
110 | valueZero();
111 | insertProperlyTemplatedBlock2(blockUuid2, sibling, data);
112 | };
113 |
114 | document.addEventListener(
115 | "keydown",
116 | function (e) {
117 | if (e.keyCode === 27) {
118 | // logseq.hideMainUI({ restoreEditingCursor: true });
119 | resetExit(e);
120 | }
121 | e.stopPropagation();
122 | },
123 | false
124 | );
125 | return (
126 |
196 | );
197 | };
198 |
199 | export default App;
200 |
--------------------------------------------------------------------------------
/src/parser.tsx:
--------------------------------------------------------------------------------
1 | import { getDateForPage } from "logseq-dateutils";
2 | import Sherlock from "sherlockjs";
3 | import { persistUUID } from "./insertUUID";
4 |
5 | import axios from "axios";
6 | import { blockUuid2, editNetworkRequest } from "./insertTemplatedBlock";
7 | // set APIKEY to be equal to the api key from github secrets
8 | const APIKEY = process.env.APIKEY;
9 | async function parseRandomly(pageName: string) {
10 | pageName.toLowerCase();
11 | let query = `[:find (pull ?b [*])
12 | :where
13 | [?b :block/path-refs [:block/name "${pageName.toLowerCase()}"]]]`;
14 |
15 | let results = await logseq.DB.datascriptQuery(query);
16 | let flattenedResults = results.map((mappedQuery) => ({
17 | uuid: mappedQuery[0].uuid,
18 | }));
19 |
20 | let index = Math.floor(Math.random() * flattenedResults.length);
21 | persistUUID(flattenedResults[index].uuid);
22 | return `((${flattenedResults[index].uuid}))`;
23 | }
24 |
25 | function parseWeather(data, format) {
26 | let emojiArray = {
27 | Clear: "🔆",
28 | Clouds: "🌥",
29 | Atmosphere: "🌫",
30 | Snow: "❄️",
31 | Rain: "🌧",
32 | Drizzle: "🌧",
33 | Thunderstorm: "⛈",
34 | }
35 | let temperature;
36 | if (format == "f") {
37 | temperature = (data.main.temp - 273.15) * 9/5 + 32
38 | temperature = Math.round((temperature + Number.EPSILON) * 100) / 100
39 | } else {
40 | temperature = data.main.temp - 273.15
41 | temperature = Math.round((temperature + Number.EPSILON) * 100) / 100
42 | }
43 | return `${temperature}°${emojiArray[data.weather[0].main]}`;
44 | }
45 | function parseConditional(condition: string, value) {
46 | switch (condition) {
47 | case "dayofweek":
48 | if (new Date().getDay() == value) {
49 | return "Success";
50 | } else if (new Date().getDay() == 0 && value == 7) {
51 | return "Success";
52 | } else {
53 | return "Error";
54 | }
55 | case "dayofmonth":
56 | if (new Date().getDate() == value) {
57 | return "Success";
58 | } else {
59 | return "Error";
60 | }
61 | case "month":
62 | if (new Date().getMonth() == value) {
63 | return "Success";
64 | } else {
65 | return "Error";
66 | }
67 | case "dayofyear":
68 | if (new Date().getDate() == value) {
69 | return "Success";
70 | } else {
71 | return "Error";
72 | }
73 | default:
74 | return "Error";
75 | }
76 | }
77 |
78 | export function parseVariablesOne(template) {}
79 | export async function parseDynamically(blockContent) {
80 |
81 | const userConfigs = await logseq.App.getUserConfigs();
82 | const preferredDateFormat = userConfigs.preferredDateFormat;
83 | let currentTime = new Date();
84 | let ifParsing = /(i+f)/gi;
85 | let pageBlock = /currentpage/gi;
86 | let uuid = /randUUID/gi;
87 | let randomParsing = /randomblock/;
88 | let shouldNotEncodeURL = true
89 | let weatherQuery = /weather/;
90 | let parsedInput;
91 | if (blockContent.match("<%%")){
92 | parsedInput = blockContent.slice(3, -2);
93 | shouldNotEncodeURL = false;
94 | }
95 | else {
96 | parsedInput = blockContent.slice(2, -2);
97 | }
98 | if (blockContent.match(ifParsing)) {
99 | let input = parsedInput.split(":");
100 | let spaceParsedInput = input[0].replace(/\s+/g, "");
101 | let input1 = spaceParsedInput.split("||");
102 | let tempStorageArray = [];
103 | for (const x in input1) {
104 | let input2 = input1[x].split("if");
105 | let input3 = input2[1].split("=");
106 | tempStorageArray.push(parseConditional(input3[0], input3[1]));
107 | }
108 | if (tempStorageArray.includes("Success")) {
109 | return input[1];
110 | } else {
111 | return "";
112 | }
113 | }
114 |
115 | if (blockContent.match(weatherQuery)) {
116 | try {
117 |
118 | let spacedSplit = parsedInput.split(" ");
119 | let weatherLocation = spacedSplit[1];
120 | let weatherFormat = spacedSplit[0].split("weather")[1];
121 | try {
122 | editNetworkRequest(true);
123 | let url = `http://api.openweathermap.org/geo/1.0/direct?q=${weatherLocation}&limit=1&appid=${APIKEY}`;
124 | let locationLongLat = await axios.get(url);
125 | let lon = locationLongLat.data[0].lon;
126 | let lat = locationLongLat.data[0].lat;
127 |
128 | let url2 = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${APIKEY}`;
129 |
130 | let weatherData = await axios.get(url2);
131 | return parseWeather(weatherData.data, weatherFormat);
132 | } catch(error) {
133 | console.log(error)
134 | return "Oops, unable to reach servers";
135 |
136 | }
137 | } catch {
138 | return "Oops, no location provided";
139 | }
140 |
141 | }
142 | if (blockContent.toLowerCase().match(pageBlock)) {
143 | // let currentp3age = await logseq.Editor.getCurrentPage();
144 | let currentp3age = await logseq.Editor.getPage((await logseq.Editor.getBlock(blockUuid2)).page.id)
145 | const inputSplit = parsedInput.split(":")
146 | if (inputSplit.length > 1) {
147 | return shouldNotEncodeURL? parseProperties((inputSplit[1]), currentp3age): encodeURIComponent(parseProperties((inputSplit[1]), currentp3age))
148 | }
149 | else{
150 | if (currentp3age != null) {
151 | return shouldNotEncodeURL? currentp3age.originalName: encodeURIComponent(currentp3age.name);
152 | } else {
153 | return shouldNotEncodeURL? getDateForPage(currentTime, preferredDateFormat): encodeURIComponent(getDateForPage(currentTime, preferredDateFormat));
154 | }}
155 | }
156 | if (blockContent.match(uuid)){
157 | return ("wxy" + Math.random().toString(36).slice(2))
158 | }
159 | if (blockContent.match(randomParsing)) {
160 | // let spaceParsedInput = parsedInput.replace(/\s+/g, '');
161 | let input2 = parsedInput.split("randomblock");
162 | let input3 = input2[1].replace(" ", "");
163 | return shouldNotEncodeURL? await parseRandomly(input3): encodeURIComponent(await parseRandomly(input3));
164 | }
165 |
166 | // Implement time parsing
167 | if (
168 | blockContent.toLowerCase() == "<%currentTime%>" ||
169 | blockContent.toLowerCase() == "<%time%>" ||
170 | blockContent.toLowerCase() == "<%current time%>"
171 | ) {
172 | let formattedTime;
173 | if (currentTime.getMinutes() < 10) {
174 | formattedTime =
175 | currentTime.getHours() + ":" + "0" + currentTime.getMinutes();
176 | } else {
177 | formattedTime = currentTime.getHours() + ":" + currentTime.getMinutes();
178 | }
179 | return shouldNotEncodeURL ? formattedTime : encodeURIComponent(formattedTime);
180 | }
181 | // Implement if parsing
182 | const parsedBlock = await Sherlock.parse(blockContent);
183 | // Destructure
184 | const { startDate } = parsedBlock;
185 |
186 | if (startDate == null) {
187 | return blockContent;
188 | }
189 | return shouldNotEncodeURL ? getDateForPage(startDate, preferredDateFormat): encodeURIComponent(getDateForPage(startDate, preferredDateFormat));
190 | }
191 |
192 | function parseProperties(text, currentPage) {
193 | const updatedText = text.replace(" ", "")
194 | //Convert dash case to camel case
195 | const camelCaseText = updatedText.replace(/-([a-z])/g, function (g) {
196 | return g[1].toUpperCase();
197 | });
198 | const propertyList = currentPage.properties[camelCaseText]
199 | if (propertyList != undefined ){
200 | return propertyList.toString()
201 | }
202 | else {
203 | return `No property exists for key ${camelCaseText}`
204 | }
205 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "@logseq/libs";
2 | import { IBatchBlock, SettingSchemaDesc } from "@logseq/libs/dist/LSPlugin.user";
3 | import Sherlock from "sherlockjs";
4 | import { getDateForPage, getDateForPageWithoutBrackets } from "logseq-dateutils";
5 | import React from "react";
6 | import ReactDOM from "react-dom";
7 | import App, { FormValues } from "./App";
8 | import { insertProperlyTemplatedBlock } from "./insertTemplatedBlock";
9 | import { updateTemplates } from "./searchbar";
10 | import SearchBar from "./searchbar";
11 | import { handleClosePopup } from "./handleClosePopup";
12 | import InsertionUI from "./inserterUI";
13 | import { uuidRegex } from "./utils";
14 | /*
15 | * main entry
16 | */
17 |
18 | export function renderApp (){
19 | handleClosePopup()
20 | ReactDOM.render(
21 |
22 |
23 | ,
24 | document.getElementById("app")
25 | );}
26 |
27 | let settings: SettingSchemaDesc[] = [
28 | {
29 | key: "Keyboard-Shortcut",
30 | type: "string",
31 | title: "Keyboard Shortcut for Triggering Smartblocks",
32 | description: "keyboard shortcut to trigger smartblock insertion window",
33 | default: "mod+t"
34 | }
35 | ]
36 | async function checkTemplate(uuid) {
37 | //Credits to Alex for this implementation https://github.com/QWxleA
38 | //is block(uuid) on a template?
39 | try {
40 | let block = await logseq.Editor.getBlock(uuid);
41 | let checkTPL =
42 | block.properties && block.properties.template != undefined ? true : false;
43 | let checkPRT =
44 | block.parent != null && block.parent.id !== block.page.id ? true : false;
45 |
46 | if (checkTPL === false && checkPRT === false) return false;
47 | if (checkTPL === true) return true;
48 | return await checkTemplate(block.parent.id);
49 | } catch (error) {
50 | console.log(error);
51 | }
52 | }
53 |
54 | export var valueCount = 0;
55 | export var valueArray: FormValues[] = [];
56 | export var currentValueCount = 0;
57 | export var currentValueArray = [];
58 |
59 | export function editValueArray(value: FormValues[]) {
60 | valueArray = value;
61 | }
62 | export function valueZero() {
63 | valueArray = [];
64 | }
65 | async function main() {
66 | updateTemplates()
67 | // logseq.App.onCurrentGraphChanged(updateTemplates)
68 | logseq.useSettingsSchema(settings);
69 | logseq.provideModel({
70 | async insertTemplatedBlock(e: any) {
71 | const { blockUuid, template, sibling, location } = e.dataset;
72 | let blockUuid2: string = blockUuid
73 | console.log(location)
74 | console.log(location.match(uuidRegex))
75 | if (location == "" || location == "undefined"){
76 | blockUuid2 = blockUuid
77 | console.log(`THis is the fun${location}`)
78 | console.log(location.match(uuidRegex))
79 | }
80 | else if (location.match(uuidRegex)!= null || (await logseq.Editor.getPage(location)!= undefined)){
81 | blockUuid2 = location
82 | console.log(`THis is the ${location}`)
83 | }
84 | else {
85 | const parsedBlock = await Sherlock.parse(location);
86 | const { isAllDay, eventTitle, startDate, endDate } = parsedBlock;
87 | blockUuid2 = getDateForPageWithoutBrackets(startDate, (await logseq.App.getUserConfigs()).preferredDateFormat)
88 | console.log(`THis is not the ${location}`)
89 | }
90 | console.log(blockUuid2)
91 | insertProperlyTemplatedBlock(blockUuid2, template, sibling);
92 | },
93 | }),
94 | logseq.provideStyle(`
95 | .templater-btn {
96 | border: 1px solid var(--ls-border-color);
97 | white-space: initial;
98 | padding: 2px 4px;
99 | border-radius: 4px;
100 | user-select: none;
101 | cursor: default;
102 | display: flex;
103 | align-content: center;
104 | }
105 |
106 | .templater-btn:hover {
107 | background-color: #defcf0;
108 | border-color: #9ddbc7;
109 | color: #0F9960;
110 | }
111 | `);
112 | logseq.Editor.registerSlashCommand("Create Inline SmartBlock", async () => {
113 | await logseq.Editor.insertAtEditingCursor(
114 | `{{renderer :smartblockInline, }} `
115 | );
116 | // templaterBlock = await logseq.Editor.getCurrentBlock();
117 | });
118 | logseq.Editor.registerSlashCommand("Insert Smartblock", async (e) => {
119 | updateTemplates();
120 | handleClosePopup()
121 | ReactDOM.render(
122 |
123 |
124 | ,
125 | document.getElementById("app")
126 | );
127 | logseq.showMainUI();
128 | console.log("Insert Smartblock");
129 | // templaterBlock = await logseq.Editor.getCurrentBlock();
130 | });
131 | logseq.Editor.registerSlashCommand(
132 | "Create Inline SmartBlock(guided)",
133 | async () => {
134 | await logseq.Editor.insertAtEditingCursor(
135 | `{{renderer :smartblockInline, template name, sibling?}} `
136 | );
137 | // templaterBlock = await logseq.Editor.getCurrentBlock();
138 | }
139 | );
140 | logseq.App.registerCommandPalette({
141 | key: 'Toggle Smartblock Inserter',
142 | label: 'Select and insert Smartblock',
143 | keybinding: {
144 | binding: logseq.settings["Keyboard-Shortcut"],
145 | mode: 'global',
146 | }
147 | }, async (e) => {
148 | updateTemplates().then(()=>{
149 | if (e.uuid != null){
150 | updateTemplates();
151 | handleClosePopup()
152 | ReactDOM.render(
153 |
154 |
155 | ,
156 | document.getElementById("app")
157 | )
158 | logseq.showMainUI()
159 | }
160 | else {
161 | logseq.UI.showMsg("Error: not in a block")
162 | }
163 | })
164 |
165 | });
166 |
167 | logseq.Editor.registerSlashCommand("Create SmartBlock Button", async (e) => {
168 | updateTemplates();
169 | ReactDOM.render(
170 |
171 |
172 | ,
173 | document.getElementById("app")
174 | )
175 | logseq.showMainUI()
176 | handleClosePopup()
177 | // templaterBlock = await logseq.Editor.getCurrentBlock();
178 | });
179 |
180 | logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => {
181 | updateTemplates();
182 | var [type, template, title, sibling, location] = payload.arguments;
183 | if (title == undefined) {
184 | title = "New Template";
185 | }
186 | let realSiblings;
187 | if (sibling == "true") {
188 | realSiblings = true;
189 | } else {
190 | realSiblings = false;
191 | }
192 | if (type == ":smartblock") {
193 | logseq.provideUI({
194 | key: `${slot}`,
195 | reset: true,
196 | slot,
197 | template: `
198 |
200 | `,
201 | });
202 | }
203 | if (type == ":smartblockInline") {
204 | if (!(await checkTemplate(payload.uuid))) {
205 | logseq.Editor.updateBlock(payload.uuid, "");
206 | await insertProperlyTemplatedBlock(payload.uuid, template, title).then(
207 | () => {
208 | logseq.Editor.updateBlock(payload.uuid, "");
209 | }
210 | );
211 | } else {
212 | logseq.provideUI({
213 | key: `${slot}`,
214 | reset: true,
215 | slot,
216 | template: `
217 |
218 | `,
219 | });
220 | }
221 | }
222 | });
223 |
224 |
225 | }
226 |
227 | logseq.ready(main).catch(console.error);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | >If this plugin helps you, I'd really appreciate your support. You can [buy me a coffee here. ](https://www.buymeacoffee.com/sawhney17)
2 | # SmartBlocks for Logseq
3 |  
4 |
5 | This plugin enables advanced templating workflows within logseq.
6 |
7 | Currently porting features from the official Smartblocks from Roam plugin. Feel free to ping me on twitter [@aryansawhney17](https://twitter.com/aryansawhney17), or file a github issue for any feedback or feature requests.
8 |
9 |
10 | # Features
11 |
12 | ## Template Inserter
13 | ### Setup
14 | 1. Use the slash menu and type `Create smartblock button block`
15 | 2. Update the values through the input
16 |
17 | ### Usage in action
18 | 
19 | 
20 | 
21 |
22 |
23 | ### Expand from smartblock expander
24 | - In version 3.0.0, you can now expand and select a smartblock _while_ inserting, similar to how Logseq's templates work.
25 | - Simply type mod+t or /insert smartblocks and navigate to the desired Smartblock and then hit enter or click it
26 | - You can search to filter in this view
27 | ### Types of expansion
28 | - You can either expand by creating a templater button or create an inline smartblock expansion
29 | - Running smartblocks expansion templater
30 | - Use the slash command to insert templater or templater guided
31 | - Follow the below syntax for flags and setup of the original template
32 | - Running smartblocks expansion inline
33 | - Use the slash command to insert templater inline
34 | - Follows slightly different syntax for flags with the format being `{{renderer :smartblockInline, templateName, sibling?}}`
35 | - Sample: `{{renderer :smartblockInline, journalTemplate, true}}`
36 | - When this is in a block marked template, then this won't auto expand to allow for usage in regular logseq templates or as a daily note template
37 | ### Flags
38 | - You can configure the templater in the following ways
39 | 1. Set template(required)
40 | 2. Set title(required for button)
41 | 3. Set sibling true or false(required for button)
42 | - Whether you want the template to be inserted as a sibling(as a new bullet _not_ a child of the button), or as a child (indented under the button)
43 | - Basic structure of the button expansion is as such
44 | - `{{renderer :smartblock, journalTemplate, New Journal, false}}`
45 |
46 | ### Accessing page properties
47 | ### Encode URL method
48 | 1. A useful application for SmartBlocks is automatically generating URLs or iFrame embeds based on things like the page name
49 | 2. In this case it becomes useful to format variables into page names.
50 | 3. Hence, by simply adding a second `%` you can have it encoded into URL format.
51 | - `<%%currentPage%>`
52 | ### Using NLP
53 | 1. In the templates, wherever you want a dynamic date, one that shows a different value based on the date it was inserted, use this syntax `<%NLP Input%>`
54 | - `<%Today%>`
55 | - `<%Last monday%>`
56 | - `<%current time%>`
57 | - `<%30th of december%>`
58 | 2. Automatically respects your date settings
59 | - Your format will automatically generate a specific date
60 | 3. Support Aliases
61 | - `[tomorrow](<%tomorrow%>)` in the template generates `[tomorrow]([[Jan 19th, 2022]])`
62 | 
63 | ### Using if statements with dates
64 | 1. If you want an item in a template to only show up on a certain day of the week
65 | 2. Follow the below syntax
66 | - if the day of the week is monday
67 | - `<%if dayofweek = 1: text to be entered%>`
68 | - if it's the 22th
69 | - `<%if dayofmonth = 22: text to be entered%>`
70 | - if it's the 100th day of the year
71 | - `<%if dayofyear = 100: text to be entered%>`
72 | - if it's July
73 | - `<%month%> = 7: text to be entered`
74 | 3. As of the latest release, you can now have OR statements to result in insertion if *any* of the properties are in action. Simply separate the parameters with `||`. Some examples are as follows
75 | - if it's either january or a monday
76 | - `<%if month = 1|| if dayofweek = 1 : text to be entered%>`
77 | - if it's a weekend
78 | - `<%if dayofweek = 6|| if dayofweek = 7 : text to be entered%>`
79 | ### Using the Random Function
80 | - Limited scope at the moment, can currently fetch a random block linking to a page and create a reference to the random block
81 | - Use ``
82 | -``
83 | -``
84 |
85 | ### Call renderers using randUUID
86 | - If you'd like to integrate things like the wordcount plugin, you can do so by generating a random alphanumeric using using <%randUUID%>
87 | - {{renderer :wordcount_<%randUUID%>}} will insert a new word counter whenever the smartblock is called provided the word counter plugin is installed
88 | ### Set inputs and use variables
89 | - You can ask the user for inputs. You can then reuse this input multiple times in the smartblock
90 | - To set an input, use ``
91 | - If you'd like you give a dropdown list of options, use `<%setInput: variableName:comma,separated,options%>`
92 | - To get the input of an already set input, i.e. if you want to use something twice, do `<%getInput: variableName%>`
93 | ### Using the weather function
94 | - Makes it possible to grab the current weather from Open Weather Map API
95 | - Format used is `<%weatherf Dubai%>`
96 | - Start with weather
97 | - Add the desired format, either `f` for farenheight or `c` for celcius
98 | - Add your current location, don't be too speciifc, weather data *may* not always be available for more specific searches
99 | - Format returned is `30° 🌧`
100 | ### Using the current Page function
101 | - If you want to import the current page into the template as a dynamic variable, simply insert the placeholder `<%currentPage%>`
102 | - Study <%currentPage%> on <%tomorrow%>
103 | - Will return `Study [[Nuclear Physics]] [[Feb 3, 2022]]`
104 |
105 | ### Limitations
106 | - ~~Only works with dynamic variables up to 4 blocks deep~~ fixed in latest update
107 |
108 | ### TODO
109 | - [x] Enable support for natural language processing for dates allowing for dynamic dates (different date auto added based on current date)
110 | - [x] Allow user to set variables via inputs
111 | - [ ] Allow NLP dates in if statements
112 |
113 | ### Using variables and inputs
114 | 
115 |
116 | - If you update to the latest version, you can create inputs and variables
117 | - To set an input use the variable <%setinput: inputName%>
118 | - When you call the smartblock, you will then be able to define inputs for those inputs, they will auto replace these blocks
119 | - To access the property of an existing input, let's say you've already set the input `people` using <%setinput: people%>
120 | - You can now access the already defined input via the line <%getinput: people%> without it prompting you again.
121 | - You can even pass dynamic variables like <%current page%> as an input!
122 | ## Property Renderer
123 | Has been shifted to it's own plugin: https://github.com/sawhney17/logseq-property-visualizer
124 |
125 |
126 | # Credits
127 | > If you like the work I'm doing, consider [buying me a coffee](https://www.buymeacoffee.com/sawhney17) :)
128 | - [Sherlockjs](https://github.com/neilgupta/Sherlock)
129 | - Thanks a ton to [hkgnp](https://github.com/hkgnp) and his [NLP plugin](https://github.com/hkgnp/logseq-datenlp-plugin) for implementation inspiration
130 | - Credits to the original SmartBlocks plugin for Roam by [@tfthacker](https://twitter.com/tfthacker)
131 | - Thanks to OpenWeatherMap for weather data
132 |
--------------------------------------------------------------------------------
/src/inserterUI.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./App.css";
3 | import "@logseq/libs";
4 | import ReactTooltip from "react-tooltip";
5 | import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
6 | import "./tailwind.css";
7 | import { smartblocks } from "./searchbar";
8 | import TextField from "@mui/material/TextField";
9 | import { uuidRegex } from "./utils";
10 |
11 |
12 |
13 |
14 | const InsertionUI: React.FC<{ blockUUID }> = (blockUUID) => {
15 | const [location, setLocation] = React.useState(null);
16 | const [sibling, setSibling] = React.useState("false");
17 | const [title, setTitle] = React.useState("");
18 | const [smartblockIndex, setSmartblockIndex] = React.useState(1)
19 | const handleChange = (e) => {
20 | console.log(e.target.id)
21 | if (e.target.id == "locationInput") {
22 | setLocation(e.target.value)
23 | }
24 | if (e.target.id == "titleInput") {
25 | setTitle(e.target.value)
26 | }
27 |
28 | if (e.target.id == "siblignSelect") {
29 | setSibling(e.target.value)
30 | }
31 | if (e.target.id == "templateSelection") {
32 | setSmartblockIndex(e.target.value)
33 | }
34 | }
35 |
36 | const [pagesList, setPageList] = React.useState([])
37 | const filter = createFilterOptions<[AutocompleteProps]>();
38 | //Fetch pages upon first load
39 | React.useEffect(() => {
40 | logseq.DB.datascriptQuery(`[:find (pull ?p [*]) :where [?p :block/uuid ?u][?p :block/name]]`).then((pages) => {
41 | let pages2 = pages.map((page) => {
42 | console.log(page[0])
43 | return { title: page[0]["original-name"] }
44 | })
45 | console.log(pages2)
46 | setPageList(pages2)
47 | }
48 | )
49 | }
50 | , [])
51 |
52 |
53 | const handleSubmit = async () => {
54 | console.log(location)
55 | let finalString;
56 |
57 | let actualLocation = location !== null ? location.title
58 | .replaceAll("(", "")
59 | .replaceAll(")", "")
60 | .replace('Block with uuid: ', "")
61 | .replace("NLP Parse: ", "")
62 | : null;
63 | let newTitle = title
64 | if (title == "") {
65 | newTitle = "New Smartblock"
66 | }
67 | console.log(smartblockIndex)
68 | console.log(smartblocks)
69 | if (actualLocation !== null) {
70 |
71 | finalString = `{{renderer :smartblock, ${smartblocks[smartblockIndex]}, ${newTitle}, ${sibling}, ${actualLocation}}}`
72 | }
73 | else {
74 | finalString = `{{renderer :smartblock, ${smartblocks[smartblockIndex]}, ${newTitle}, ${sibling}}}`
75 | }
76 | console.log(blockUUID.blockUUID)
77 | // let block = (await logseq.Editor.getBlock(blockUUID.blockUUID)).content
78 | // logseq.Editor.updateBlock(blockUUID.blockUUID, `${block} ${finalString}`)
79 | logseq.Editor.insertAtEditingCursor(finalString)
80 | logseq.hideMainUI()
81 | }
82 | return (
83 |
84 |
85 |
Insert Smartblock
86 |
186 |
187 |
188 |
189 |
190 | );
191 | };
192 |
193 | export default InsertionUI;
194 |
195 | interface AutocompleteProps {
196 | inputValue?: string;
197 | title: string;
198 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.0.17 | MIT License | https://tailwindcss.com
3 | *//*
4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
6 | */
7 |
8 | *,
9 |
10 | ::before,
11 | ::after {
12 | box-sizing: border-box; /* 1 */
13 | border-width: 0; /* 2 */
14 | border-style: solid; /* 2 */
15 | border-color: #e5e7eb; /* 2 */
16 |
17 | }
18 |
19 | ::before,
20 | ::after {
21 | --tw-content: '';
22 | }
23 |
24 | /*
25 | 1. Use a consistent sensible line-height in all browsers.
26 | 2. Prevent adjustments of font size after orientation changes in iOS.
27 | 3. Use a more readable tab size.
28 | 4. Use the user's configured `sans` font-family by default.
29 | */
30 |
31 | html {
32 | line-height: 1.5; /* 1 */
33 | -webkit-text-size-adjust: 100%; /* 2 */
34 | -moz-tab-size: 4; /* 3 */
35 | -o-tab-size: 4;
36 | tab-size: 4; /* 3 */
37 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */;
38 | --cl-charcoal: #3f3f3f;
39 | --cl-darkjungle: #1e1e1e;
40 | --cl-thunder: #4c4c4c;
41 |
42 |
43 | }
44 |
45 | /*
46 | 1. Remove the margin in all browsers.
47 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
48 | */
49 |
50 | body {
51 | margin: 0; /* 1 */
52 | line-height: inherit; /* 2 */
53 | }
54 |
55 | /*
56 | 1. Add the correct height in Firefox.
57 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
58 | 3. Ensure horizontal rules are visible by default.
59 | */
60 |
61 | hr {
62 | height: 0; /* 1 */
63 | color: inherit; /* 2 */
64 | border-top-width: 1px; /* 3 */
65 | }
66 |
67 | /*
68 | Add the correct text decoration in Chrome, Edge, and Safari.
69 | */
70 |
71 | abbr:where([title]) {
72 | -webkit-text-decoration: underline dotted;
73 | text-decoration: underline dotted;
74 | }
75 |
76 | /*
77 | Remove the default font size and weight for headings.
78 | */
79 |
80 | h1,
81 | h2,
82 | h3,
83 | h4,
84 | h5,
85 | h6 {
86 | font-size: inherit;
87 | font-weight: inherit;
88 | }
89 |
90 | /*
91 | Reset links to optimize for opt-in styling instead of opt-out.
92 | */
93 |
94 | a {
95 | color: inherit;
96 | text-decoration: inherit;
97 | }
98 |
99 | /*
100 | Add the correct font weight in Edge and Safari.
101 | */
102 |
103 | b,
104 | strong {
105 | font-weight: bolder;
106 | }
107 |
108 | /*
109 | 1. Use the user's configured `mono` font family by default.
110 | 2. Correct the odd `em` font sizing in all browsers.
111 | */
112 |
113 | code,
114 | kbd,
115 | samp,
116 | pre {
117 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
118 | font-size: 1em; /* 2 */
119 | }
120 |
121 | /*
122 | Add the correct font size in all browsers.
123 | */
124 |
125 | small {
126 | font-size: 80%;
127 | }
128 |
129 | /*
130 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
131 | */
132 |
133 | sub,
134 | sup {
135 | font-size: 75%;
136 | line-height: 0;
137 | position: relative;
138 | vertical-align: baseline;
139 | }
140 |
141 | sub {
142 | bottom: -0.25em;
143 | }
144 |
145 | sup {
146 | top: -0.5em;
147 | }
148 |
149 | /*
150 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
151 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
152 | 3. Remove gaps between table borders by default.
153 | */
154 |
155 | table {
156 | text-indent: 0; /* 1 */
157 | border-color: inherit; /* 2 */
158 | border-collapse: collapse; /* 3 */
159 | }
160 |
161 | /*
162 | 1. Change the font styles in all browsers.
163 | 2. Remove the margin in Firefox and Safari.
164 | 3. Remove default padding in all browsers.
165 | */
166 |
167 | button,
168 | input,
169 | optgroup,
170 | select,
171 | textarea {
172 | font-family: inherit; /* 1 */
173 | font-size: 100%; /* 1 */
174 | line-height: inherit; /* 1 */
175 | color: inherit; /* 1 */
176 | margin: 0; /* 2 */
177 | padding: 0; /* 3 */
178 | }
179 |
180 | /*
181 | Remove the inheritance of text transform in Edge and Firefox.
182 | */
183 |
184 | button,
185 | select {
186 | text-transform: none;
187 | }
188 |
189 | /*
190 | 1. Correct the inability to style clickable types in iOS and Safari.
191 | 2. Remove default button styles.
192 | */
193 |
194 | button,
195 | [type='button'],
196 | [type='reset'],
197 | [type='submit'] {
198 | -webkit-appearance: button; /* 1 */
199 | background-color: transparent; /* 2 */
200 | background-image: none; /* 2 */
201 | }
202 |
203 | /*
204 | Use the modern Firefox focus style for all focusable elements.
205 | */
206 |
207 | :-moz-focusring {
208 | outline: auto;
209 | }
210 |
211 | /*
212 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
213 | */
214 |
215 | :-moz-ui-invalid {
216 | box-shadow: none;
217 | }
218 |
219 | /*
220 | Add the correct vertical alignment in Chrome and Firefox.
221 | */
222 |
223 | progress {
224 | vertical-align: baseline;
225 | }
226 |
227 | /*
228 | Correct the cursor style of increment and decrement buttons in Safari.
229 | */
230 |
231 | ::-webkit-inner-spin-button,
232 | ::-webkit-outer-spin-button {
233 | height: auto;
234 | }
235 |
236 | /*
237 | 1. Correct the odd appearance in Chrome and Safari.
238 | 2. Correct the outline style in Safari.
239 | */
240 |
241 | [type='search'] {
242 | -webkit-appearance: textfield; /* 1 */
243 | outline-offset: -2px; /* 2 */
244 | }
245 |
246 | /*
247 | Remove the inner padding in Chrome and Safari on macOS.
248 | */
249 |
250 | ::-webkit-search-decoration {
251 | -webkit-appearance: none;
252 | }
253 |
254 | /*
255 | 1. Correct the inability to style clickable types in iOS and Safari.
256 | 2. Change font properties to `inherit` in Safari.
257 | */
258 |
259 | ::-webkit-file-upload-button {
260 | -webkit-appearance: button; /* 1 */
261 | font: inherit; /* 2 */
262 | }
263 |
264 | /*
265 | Add the correct display in Chrome and Safari.
266 | */
267 |
268 | summary {
269 | display: list-item;
270 | }
271 |
272 | /*
273 | Removes the default spacing and border for appropriate elements.
274 | */
275 |
276 | blockquote,
277 | dl,
278 | dd,
279 | h1,
280 | h2,
281 | h3,
282 | h4,
283 | h5,
284 | h6,
285 | hr,
286 | figure,
287 | p,
288 | pre {
289 | margin: 0;
290 | }
291 |
292 | fieldset {
293 | margin: 0;
294 | padding: 0;
295 | }
296 |
297 | legend {
298 | padding: 0;
299 | }
300 |
301 | ol,
302 | ul,
303 | menu {
304 | list-style: none;
305 | margin: 0;
306 | padding: 0;
307 | }
308 |
309 | /*
310 | Prevent resizing textareas horizontally by default.
311 | */
312 |
313 | textarea {
314 | resize: vertical;
315 | }
316 |
317 | /*
318 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
319 | 2. Set the default placeholder color to the user's configured gray 400 color.
320 | */
321 |
322 | input::-moz-placeholder, textarea::-moz-placeholder {
323 | opacity: 1; /* 1 */
324 | color: #9ca3af; /* 2 */
325 | }
326 |
327 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
328 | opacity: 1; /* 1 */
329 | color: #9ca3af; /* 2 */
330 | }
331 |
332 | input::placeholder,
333 | textarea::placeholder {
334 | opacity: 1; /* 1 */
335 | color: #9ca3af; /* 2 */
336 | }
337 |
338 | /*
339 | Set the default cursor for buttons.
340 | */
341 |
342 | button,
343 | [role="button"] {
344 | cursor: pointer;
345 | }
346 |
347 | /*
348 | Make sure disabled buttons don't get the pointer cursor.
349 | */
350 | :disabled {
351 | cursor: default;
352 | }
353 |
354 | /*
355 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
356 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
357 | This can trigger a poorly considered lint error in some tools but is included by design.
358 | */
359 |
360 | img,
361 | svg,
362 | video,
363 | canvas,
364 | audio,
365 | iframe,
366 | embed,
367 | object {
368 | display: block; /* 1 */
369 | vertical-align: middle; /* 2 */
370 | }
371 |
372 | /*
373 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
374 | */
375 |
376 | img,
377 | video {
378 | max-width: 100%;
379 | height: auto;
380 | }
381 |
382 | /*
383 | Ensure the default browser behavior of the `hidden` attribute.
384 | */
385 |
386 | [hidden] {
387 | display: none;
388 | }
389 |
390 | *, ::before, ::after {
391 | --tw-translate-x: 0;
392 | --tw-translate-y: 0;
393 | --tw-rotate: 0;
394 | --tw-skew-x: 0;
395 | --tw-skew-y: 0;
396 | --tw-scale-x: 1;
397 | --tw-scale-y: 1;
398 | --tw-pan-x: ;
399 | --tw-pan-y: ;
400 | --tw-pinch-zoom: ;
401 | --tw-scroll-snap-strictness: proximity;
402 | --tw-ordinal: ;
403 | --tw-slashed-zero: ;
404 | --tw-numeric-figure: ;
405 | --tw-numeric-spacing: ;
406 | --tw-numeric-fraction: ;
407 | --tw-ring-inset: ;
408 | --tw-ring-offset-width: 0px;
409 | --tw-ring-offset-color: #fff;
410 | --tw-ring-color: rgb(59 130 246 / 0.5);
411 | --tw-ring-offset-shadow: 0 0 #0000;
412 | --tw-ring-shadow: 0 0 #0000;
413 | --tw-shadow: 0 0 #0000;
414 | --tw-shadow-colored: 0 0 #0000;
415 | --tw-blur: ;
416 | --tw-brightness: ;
417 | --tw-contrast: ;
418 | --tw-grayscale: ;
419 | --tw-hue-rotate: ;
420 | --tw-invert: ;
421 | --tw-saturate: ;
422 | --tw-sepia: ;
423 | --tw-drop-shadow: ;
424 | --tw-backdrop-blur: ;
425 | --tw-backdrop-brightness: ;
426 | --tw-backdrop-contrast: ;
427 | --tw-backdrop-grayscale: ;
428 | --tw-backdrop-hue-rotate: ;
429 | --tw-backdrop-invert: ;
430 | --tw-backdrop-opacity: ;
431 | --tw-backdrop-saturate: ;
432 | --tw-backdrop-sepia: ;
433 | }
434 | .absolute {
435 | position: absolute;
436 | }
437 | .top-10 {
438 | top: 2.5rem;
439 | }
440 | .mr-3 {
441 | margin-right: 0.75rem;
442 | }
443 | .flex {
444 | display: flex;
445 | }
446 | .w-1\/3 {
447 | width: 33.333333%;
448 | }
449 | .w-full {
450 | width: 100%;
451 | }
452 | .appearance-none {
453 | -webkit-appearance: none;
454 | -moz-appearance: none;
455 | appearance: none;
456 | }
457 | .justify-center {
458 | justify-content: center;
459 | }
460 | .rounded-lg {
461 | border-radius: 0.5rem;
462 | }
463 | .border {
464 | border-width: 1px;
465 | }
466 | .border-none {
467 | border-style: none;
468 | }
469 | .border-black {
470 | --tw-border-opacity: 1;
471 | border-color: rgb(0 0 0 / var(--tw-border-opacity));
472 | }
473 | .bg-white {
474 | --tw-bg-opacity: 1;
475 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
476 | }
477 | .bg-transparent {
478 | background-color: transparent;
479 | }
480 | .p-3 {
481 | padding: 0.75rem;
482 | }
483 | .py-1 {
484 | padding-top: 0.25rem;
485 | padding-bottom: 0.25rem;
486 | }
487 | .px-2 {
488 | padding-left: 0.5rem;
489 | padding-right: 0.5rem;
490 | }
491 | .leading-tight {
492 | line-height: 1.25;
493 | }
494 | .text-gray-700 {
495 | --tw-text-opacity: 1;
496 | color: rgb(55 65 81 / var(--tw-text-opacity));
497 | }
498 | .focus\:outline-none:focus {
499 | outline: 2px solid transparent;
500 | outline-offset: 2px;
501 | }
502 |
503 |
504 | body {font-family: Arial, Helvetica, sans-serif;}
505 | * {box-sizing: border-box;}
506 |
507 | .form-inline {
508 | display: flex;
509 | flex-flow: row wrap;
510 | align-items: center;
511 | }
512 |
513 | .form-inline label {
514 | margin: 5px 10px 5px 0;
515 | }
516 |
517 | .form-inline input {
518 | vertical-align: middle;
519 | margin: 5px 10px 5px 0;
520 | padding: 10px;
521 | background-color: #fff;
522 | border: 1px solid #ddd;
523 | }
524 |
525 | .button-section {
526 | margin: 3%;
527 | }
528 | .button {
529 | padding: 10px 20px;
530 | /* background-color: dodgerblue; */
531 | border: 1px solid #ddd;
532 | color: white;
533 | cursor: pointer;
534 | border-radius: 10px;
535 | }
536 |
537 | .button:hover {
538 | background-color: rgb(0, 0, 0);
539 | }
540 |
541 | .remove {
542 | background-color: rgb(192, 53, 53);
543 | }
544 | .remove:hover {
545 | background-color: rgb(187, 43, 43);
546 | }
547 | .overlay {
548 | position: fixed;
549 | top: 0;
550 | left: 0;
551 | width: 100vw;
552 | height: 100vh;
553 | background: hsla(0,0%,10%,.5);
554 | z-index: 9;
555 | }
556 | .smartblock-popup {
557 | padding: 1em;
558 | background: rgb(40, 157, 165);
559 | z-index: 9;
560 | border-radius: 10px;
561 |
562 | /* -ms-transform: translate(-50%, -50%);
563 | transform: translate(-50%, -50%); */
564 | }
565 | .popup-content {
566 | z-index: 10;
567 | }
568 |
569 | .centered-element {
570 | margin: 0;
571 | position: absolute;
572 | top: 30%;
573 | transform: translateY(-50%);
574 | }
575 |
576 | .grid-container {
577 | display: grid;
578 | grid-template-columns: 1fr 1fr;
579 | grid-gap: 20px;
580 | padding-top: 10px;
581 | }
582 | .label-class {
583 | color: whitesmoke;
584 | font-family: system-ui;
585 | padding-bottom: 10px;
586 | font-size: larger;}
587 | .smartblock-inserter {
588 | padding: 1em;
589 | /* background: rgb(40, 157, 165); */
590 | z-index: 9;
591 | border-radius: 15px;
592 | top: 30%;
593 | width: 50%;
594 | display: flex;
595 | transform: translate(0, 100px);
596 | flex-direction: column;
597 | color: #ffffff;
598 | align-items: center;
599 | background-color: var(--cl-darkjungle)!important;
600 | border-top: 1px solid var(--cl-charcoal);
601 | border-right: 1px solid var(--cl-charcoal);
602 | border-bottom: 1px solid var(--cl-charcoal);
603 | border-left: 1px solid var(--cl-charcoal);
604 | font-size: 1.2em;
605 | font-family: system-ui;
606 | padding: 0.5em;
607 | border-radius: 5px;
608 | outline: none;
609 | /* box-shadow: 0 0 0 0 transparent; */
610 | /* transition: box-shadow 0.3s ease-in-out; */
611 | }
612 | .cp__palette-input{
613 | width: 100%;
614 | height: 100%;
615 | border: none;
616 | background: transparent;
617 | color: #ffffff !important;
618 | font-size: 1.2em;
619 | font-family: system-ui;
620 | padding: 0.5em;
621 | border-radius: 5px;
622 | outline: none;
623 | box-shadow: 0 0 0 0 transparent;
624 | transition: box-shadow 0.3s ease-in-out;
625 | }
626 | .searchItem{
627 | color: #ffffff
628 | }
629 | .css-1na68kg .MuiOutlinedInput-root {
630 | /* padding: 0px; */
631 | }
632 |
633 | select {
634 | color: black !important;
635 | }
636 |
637 | input {
638 | color: black !important;
639 | }
--------------------------------------------------------------------------------