0){if(++t>=n)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}},function(e,t,n){var r=n(20),o=n(1),i=n(13),a=n(5);e.exports=function(e,t,n){if(!a(n))return!1;var u=typeof t;return!!("number"==u?o(n)&&i(t,n.length):"string"==u&&t in n)&&r(n[t],e)}},function(e,t,n){"use strict";(function(e){Object.defineProperty(t,"__esModule",{value:!0});var r,o=n(7),i=(r=o)&&r.__esModule?r:{default:r};t.default=function(t){try{(0,i.default)(t,function(e){return e.selected=!0})}catch(t){e.log(t)}}}).call(t,n(0))},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){var t=parseInt(e.substring(0,2),16)/255,n=parseInt(e.substring(2,4),16)/255,r=parseInt(e.substring(4,6),16)/255;return NSColor.colorWithRed_green_blue_alpha(t,n,r,1)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(e))}}]);"default"===key&&"function"==typeof exports?exports(context):exports[key](context)}that.onRun=__skpm_run.bind(this,"default");
--------------------------------------------------------------------------------
/SketchSelect.sketchplugin/Contents/Sketch/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Sketch Select",
3 | "identifier": "cm.sketch.select",
4 | "compatibleVersion": 3,
5 | "bundleVersion": 1,
6 | "icon": "icon.png",
7 | "commands": [
8 | {
9 | "name": "🖱 Select",
10 | "script": "index.js",
11 | "identifier": "sketch-select",
12 | "shortcut": "command shift f"
13 | }
14 | ],
15 | "menu": {
16 | "title": "Select",
17 | "isRoot": true,
18 | "items": [
19 | "sketch-select"
20 | ]
21 | },
22 | "version": "3.0.0",
23 | "description": "Make it convenient to select layers with similar attributes.",
24 | "homepage": "https://github.com/canisminor1990/sketch-select",
25 | "disableCocoaScriptPreprocessor": true,
26 | "appcast": "https://raw.githubusercontent.com/canisminor1990/sketch-select/master/.appcast.xml",
27 | "author": "CanisMinor",
28 | "authorEmail": "i@canisminor.cc"
29 | }
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/icon.png
--------------------------------------------------------------------------------
/img/rm-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/img/rm-banner.png
--------------------------------------------------------------------------------
/img/rm-dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/img/rm-dialog.png
--------------------------------------------------------------------------------
/img/rm-option.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/img/rm-option.png
--------------------------------------------------------------------------------
/img/rm-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/img/rm-run.png
--------------------------------------------------------------------------------
/img/rm-shortkey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/img/rm-shortkey.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sketch-select",
3 | "version": "3.0.1",
4 | "description": "Make it convenient to select layers with similar attributes.",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/canisminor1990/sketch-select.git"
8 | },
9 | "author": {
10 | "name": "CanisMinor",
11 | "email": "i@canisminor.cc",
12 | "url": "https://canisminor.cc/"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/canisminor1990/sketch-select/issues"
17 | },
18 | "homepage": "https://github.com/canisminor1990/sketch-select",
19 | "main": "SketchSelect.sketchplugin",
20 | "engines": {
21 | "sketch": ">=49.0"
22 | },
23 | "skpm": {
24 | "name": "sketch-select",
25 | "manifest": "src/manifest.json",
26 | "main": "SketchSelect.sketchplugin",
27 | "assets": [
28 | "dist/**/*"
29 | ]
30 | },
31 | "appcast": "https://raw.githubusercontent.com/canisminor1990/sketch-select/master/.appcast.xml",
32 | "scripts": {
33 | "start": "concurrently \"yarn start:panel\" \"yarn start:plugin\"",
34 | "start:plugin": "cross-env NODE_ENV=development skpm-build --watch",
35 | "start:panel": "roadhog dev",
36 | "build": "yarn build:panel && yarn build:plugin",
37 | "build:plugin": "cross-env NODE_ENV=production skpm-build",
38 | "build:panel": "roadhog build",
39 | "publish": "skpm publish",
40 | "link": "skpm-link",
41 | "lint": "lint-staged",
42 | "lint:es": "eslint --fix --ext .js ./"
43 | },
44 | "pre-commit": [
45 | "lint"
46 | ],
47 | "lint-staged": {
48 | "*.md": [
49 | "prettier --trailing-comma all --single-quote --write",
50 | "git add"
51 | ],
52 | "./package.json": [
53 | "prettier --trailing-comma all --single-quote --write",
54 | "git add"
55 | ],
56 | "src/**/*.js": [
57 | "eslint --fix",
58 | "git add"
59 | ],
60 | "panel/**/*.js": [
61 | "eslint --fix",
62 | "git add"
63 | ],
64 | "panel/**/*.scss": [
65 | "prettier --trailing-comma all --single-quote --write",
66 | "git add"
67 | ]
68 | },
69 | "peerDependencies": {
70 | "skpm": "^1.0.14"
71 | },
72 | "dependencies": {
73 | "@skpm/builder": "^0.4.0",
74 | "antd": "^3.2.3",
75 | "dva": "^2.1.0",
76 | "dva-loading": "^1.0.4",
77 | "lodash": "^4.17.4",
78 | "react": "^16.2.0",
79 | "react-dom": "^16.2.0",
80 | "sketch-module-web-view": "^0.2.6"
81 | },
82 | "devDependencies": {
83 | "babel-core": "^6.26.0",
84 | "babel-plugin-dva-hmr": "^0.4.0",
85 | "babel-plugin-import": "^1.6.2",
86 | "babel-plugin-lodash": "^3.2.11",
87 | "babel-preset-env": "^1.6.1",
88 | "babel-preset-stage-0": "^6.24.1",
89 | "concurrently": "^3.5.1",
90 | "cross-env": "^5.1.3",
91 | "eslint": "^4.13.1",
92 | "eslint-config-prettier": "^2.9.0",
93 | "eslint-config-standard": "^10.2.1",
94 | "eslint-plugin-flowtype": "^2.37.0",
95 | "eslint-plugin-import": "^2.7.0",
96 | "eslint-plugin-node": "^5.2.0",
97 | "eslint-plugin-prettier": "^2.3.1",
98 | "eslint-plugin-promise": "^3.5.0",
99 | "eslint-plugin-react": "^7.4.0",
100 | "eslint-plugin-standard": "^3.0.1",
101 | "expect": "^21.2.1",
102 | "husky": "^0.14.3",
103 | "lint-staged": "^4.2.3",
104 | "node-sass": "^4.5.3",
105 | "pre-commit": "^1.2.2",
106 | "prettier": "^1.11.1",
107 | "redbox-react": "^1.3.2",
108 | "roadhog": "^2.2.0",
109 | "sass-loader": "^6.0.6"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/panel/components/Copyright/index.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { Icon } from 'antd';
3 | import pluginCall from 'sketch-module-web-view/client';
4 | import style from './index.scss';
5 |
6 | export default class extends Component {
7 | openWeb = () => {
8 | console.log(this.props);
9 | const { src } = this.props;
10 | pluginCall('openWeb', src);
11 | console.log('openWeb = ', src);
12 | };
13 |
14 | render() {
15 | return (
16 |
17 |
18 | canisminor1990
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/panel/components/Copyright/index.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 100%;
6 | border: none;
7 | color: #ccc;
8 | margin-top: 12px;
9 | font-size: 13px;
10 | cursor: pointer;
11 | > span {
12 | margin-left: 8px;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/panel/components/Selection/index.js:
--------------------------------------------------------------------------------
1 | import { Checkbox, Input } from 'antd';
2 | import { connect } from 'dva';
3 | import style from './index.scss';
4 |
5 | const State = ({ config }) => ({ config });
6 |
7 | const Selection = ({ dispatch, config, title, type }) => {
8 | const update = (data, type = 'config') => {
9 | dispatch({ type: `${type}/update`, payload: data });
10 | };
11 | const onCheck = (e, type) => {
12 | const checked = e.target.checked;
13 | update({ [`${type}Checked`]: checked }, 'config');
14 | };
15 | const onInputChange = (e, type) => {
16 | const value = e.target.value;
17 | update({ [type]: value }, 'config');
18 | };
19 |
20 | return (
21 | onCheck(e, type)}
25 | >
26 | onInputChange(e, type)} />
27 |
28 | );
29 | };
30 |
31 | export default connect(State)(Selection);
32 |
--------------------------------------------------------------------------------
/panel/components/Selection/index.scss:
--------------------------------------------------------------------------------
1 | .selection {
2 | display: block;
3 | margin-left: 0 !important;
4 | display: flex;
5 | align-items: center;
6 | margin-bottom: 0.5rem;
7 | > span:last-child {
8 | flex: 1;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/panel/components/Switchbox/index.js:
--------------------------------------------------------------------------------
1 | import { Switch } from 'antd';
2 | import { connect } from 'dva';
3 | import style from './index.scss';
4 |
5 | const State = ({ config }) => ({ config });
6 |
7 | const Switchbox = ({ dispatch, config, title, type }) => {
8 | const update = (data, type = 'config') => {
9 | dispatch({ type: `${type}/update`, payload: data });
10 | };
11 | const onSwitch = (checked, type) => {
12 | update({ [`${type}Switch`]: checked }, 'config');
13 | };
14 |
15 | return (
16 |
17 | onSwitch(e, type)} />
18 | {title}
19 |
20 | );
21 | };
22 |
23 | export default connect(State)(Switchbox);
24 |
--------------------------------------------------------------------------------
/panel/components/Switchbox/index.scss:
--------------------------------------------------------------------------------
1 | .globalSwitch {
2 | display: flex;
3 | align-items: center;
4 | > span {
5 | margin-right: 0.5rem;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/panel/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Copyright } from './Copyright';
2 | export { default as Selection } from './Selection';
3 | export { default as Switchbox } from './Switchbox';
4 |
--------------------------------------------------------------------------------
/panel/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 | Sketch Select
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/panel/index.js:
--------------------------------------------------------------------------------
1 | import { message } from 'antd';
2 | import dva from 'dva';
3 | import createLoading from 'dva-loading';
4 | import './index.scss';
5 |
6 | // 1. Initialize
7 | const app = dva({
8 | onError(e) {
9 | message.error(e.message, 3);
10 | },
11 | });
12 |
13 | // 2. Models
14 | app.model(require('./models/config').default);
15 | app.model(require('./models/layers').default);
16 |
17 | // 2. Plugins
18 | app.use(createLoading());
19 |
20 | // 3. Router
21 | app.router(require('./router').default);
22 |
23 | // 4. Start
24 | app.start('#root');
25 |
26 | // 5. Sketch
27 | window.getSelection = json => localStorage.setItem('selection', JSON.stringify(json));
28 |
29 | // Disable the context menu to have a more native feel
30 | document.addEventListener('contextmenu', e => e.preventDefault());
31 |
--------------------------------------------------------------------------------
/panel/index.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | :global(#root) {
4 | height: 100%;
5 | background: #fff;
6 | overflow: hidden;
7 | user-select: none;
8 | }
9 |
10 | *:focus {
11 | outline: none;
12 | }
13 |
14 | * {
15 | -webkit-font-smoothing: antialiased;
16 | }
17 |
18 | div {
19 | box-sizing: border-box;
20 | position: relative;
21 | }
22 |
23 | h1 {
24 | font-size: 1rem;
25 | margin: 0.5rem 0 1rem;
26 | }
27 |
--------------------------------------------------------------------------------
/panel/models/config.js:
--------------------------------------------------------------------------------
1 | const Checked = type => ({
2 | [type]: '',
3 | [`${type}Checked`]: false,
4 | });
5 | const Switch = type => ({
6 | [`${type}Switch`]: false,
7 | });
8 |
9 | const defaultConfig = {
10 | ...Switch('global'),
11 | ...Checked('type'),
12 | ...Checked('name'),
13 | ...Checked('id'),
14 | // rect
15 | ...Checked('x'),
16 | ...Checked('y'),
17 | ...Checked('width'),
18 | ...Checked('height'),
19 | // text
20 | ...Checked('text'),
21 | ...Switch('textReg'),
22 | ...Checked('alignment'),
23 | ...Checked('lineSpacing'),
24 | ...Checked('fixedWidth'),
25 | // shape
26 | ...Checked('fillColor'),
27 | ...Checked('fillType'),
28 | ...Checked('borderColor'),
29 | ...Checked('borderThickness'),
30 | // symbol
31 | ...Checked('symbolId'),
32 | // other
33 | ...Switch('exportable'),
34 | ...Switch('hide'),
35 | ...Switch('lock'),
36 | };
37 | export default {
38 | namespace: 'config',
39 |
40 | state: {
41 | page: 'Any',
42 | ...defaultConfig,
43 | },
44 |
45 | reducers: {
46 | updateSuccess(state, action) {
47 | const payload = action.payload;
48 | return { ...state, ...payload };
49 | },
50 | reset(state, action) {
51 | return { ...state, ...defaultConfig };
52 | },
53 | },
54 |
55 | effects: {
56 | *update(action, { put }) {
57 | const payload = action.payload;
58 | console.log('update', payload);
59 | yield put({ type: 'updateSuccess', payload });
60 | },
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/panel/models/layers.js:
--------------------------------------------------------------------------------
1 | const defaultConfig = {
2 | checkedList: [],
3 | indeterminate: false,
4 | checkAll: false,
5 | };
6 |
7 | export default {
8 | namespace: 'layers',
9 |
10 | state: {
11 | options: [
12 | 'Artboard',
13 | 'Page',
14 | 'Group',
15 | 'Text',
16 | 'Shape',
17 | 'Image',
18 | 'SymbolInstance',
19 | 'SymbolMaster',
20 | 'SymbolOverride',
21 | ],
22 | ...defaultConfig,
23 | },
24 |
25 | reducers: {
26 | reset(state, action) {
27 | return { ...state, ...defaultConfig };
28 | },
29 | updateSuccess(state, action) {
30 | const payload = action.payload;
31 | return { ...state, ...payload };
32 | },
33 | },
34 |
35 | effects: {
36 | *update(action, { put }) {
37 | const payload = action.payload;
38 | console.log('update', payload);
39 | yield put({ type: 'updateSuccess', payload });
40 | },
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/panel/router.js:
--------------------------------------------------------------------------------
1 | import { Route, Router } from 'dva/router';
2 | import Panel from './routes';
3 |
4 | export default ({ app, history }) => {
5 | history.listen(() => window.scrollTo(0, 0));
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/panel/routes/Any/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../Tab';
2 | import { connect } from 'dva';
3 | import { Collapse, Checkbox } from 'antd';
4 | import style from '../index.scss';
5 | const Panel = Collapse.Panel;
6 |
7 | const CheckboxGroup = Checkbox.Group;
8 |
9 | class Page extends Tab {
10 | // Components
11 | LayerTypes = () => {
12 | const layers = this.props.layers;
13 | return (
14 |
15 |
16 |
21 | Check all
22 |
23 |
24 |
29 |
30 | );
31 | };
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | // Func
50 | onLayersChange = list => {
51 | const optLength = this.props.layers.options.length;
52 | const length = list.length;
53 | const Layer = {
54 | checkedList: list,
55 | indeterminate: !!length && length < optLength,
56 | checkAll: length === optLength,
57 | };
58 | this.props.update(Layer, 'layers');
59 | };
60 | onCheckAllLayers = e => {
61 | const checked = e.target.checked;
62 | const options = this.props.layers.options;
63 | const Layer = {
64 | checkedList: checked ? options : [],
65 | indeterminate: false,
66 | checkAll: checked,
67 | };
68 | this.props.update(Layer, 'layers');
69 | };
70 | }
71 |
72 | export default connect(Tab.State, Tab.Func)(Page);
73 |
--------------------------------------------------------------------------------
/panel/routes/Shape/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../Tab';
2 | import { connect } from 'dva';
3 | import { Collapse } from 'antd';
4 | import style from '../index.scss';
5 | const Panel = Collapse.Panel;
6 |
7 | class Page extends Tab {
8 | // Components
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | export default connect(Tab.State, Tab.Func)(Page);
28 |
--------------------------------------------------------------------------------
/panel/routes/Symbol/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../Tab';
2 | import { connect } from 'dva';
3 | import { Collapse } from 'antd';
4 | import { Selection } from '../../components';
5 | import style from '../index.scss';
6 | const Panel = Collapse.Panel;
7 |
8 | class Page extends Tab {
9 | // Components
10 | Symbol = () => (
11 |
12 |
13 |
14 | );
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export default connect(Tab.State, Tab.Func)(Page);
34 |
--------------------------------------------------------------------------------
/panel/routes/Tab/index.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { Switchbox, Selection } from '../../components';
3 |
4 | class Tab extends Component {
5 | // Components
6 |
7 | Name = () => {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | Rectangle = () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | Style = () => {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | Other = () => {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 | }
48 |
49 | Tab.State = state => ({
50 | ...state,
51 | });
52 |
53 | Tab.Func = dispatch => ({
54 | update(data, type = 'config') {
55 | dispatch({ type: `${type}/update`, payload: data });
56 | },
57 | reset() {
58 | dispatch({ type: `config/reset` });
59 | dispatch({ type: `layers/reset` });
60 | },
61 | });
62 |
63 | export default Tab;
64 |
--------------------------------------------------------------------------------
/panel/routes/Tab/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "type" : "Shape",
3 | "id" : "38D53BC0-823D-4376-A038-B24D82A274F3",
4 | "frame" : {
5 | "x" : 56,
6 | "y" : 41,
7 | "width" : 150,
8 | "height": 108
9 | },
10 | "name" : "Rectangle",
11 | "selected": true,
12 | "flow" : null,
13 | "style" : {
14 | "type" : "Style",
15 | "id" : "826FDC94-2F8A-4151-A5A0-3A1237F2AF9F",
16 | "fills" : [
17 | {
18 | "color": "#d8d8d8ff",
19 | "fill" : "color"
20 | }
21 | ],
22 | "borders": [
23 | {
24 | "color" : "#979797ff",
25 | "fillType" : "color",
26 | "position" : "Inside",
27 | "thickness": 1
28 | }
29 | ]
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/panel/routes/Text/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../Tab';
2 | import { connect } from 'dva';
3 | import { Collapse } from 'antd';
4 | import { Selection, Switchbox } from '../../components';
5 | import style from '../index.scss';
6 | const Panel = Collapse.Panel;
7 |
8 | class Page extends Tab {
9 | // Components
10 |
11 | Content = () => (
12 |
13 |
14 |
15 |
16 | );
17 |
18 | Prototyping = () => (
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | export default connect(Tab.State, Tab.Func)(Page);
47 |
--------------------------------------------------------------------------------
/panel/routes/_reset.scss:
--------------------------------------------------------------------------------
1 | .ant-tabs-tab {
2 | margin: 0 0.5rem !important;
3 | }
4 | .ant-checkbox-group {
5 | display: flex;
6 | flex-wrap: wrap;
7 | }
8 | .ant-checkbox-group-item {
9 | width: 45%;
10 | }
11 | .ant-collapse-header {
12 | padding-left: 0 !important;
13 | font-size: 0.95rem;
14 | color: #222;
15 | font-weight: 600;
16 | &:after {
17 | content: '#';
18 | color: #e3e3e3;
19 | margin-left: 0.5rem;
20 | }
21 | > i {
22 | right: 0;
23 | left: auto !important;
24 | color: #999;
25 | }
26 | }
27 | .ant-collapse-item {
28 | border: none !important;
29 | }
30 | .ant-collapse-content {
31 | border: none !important;
32 | padding: 0 !important;
33 | }
34 | .ant-checkbox-wrapper {
35 | .ant-input {
36 | border-radius: 2rem !important;
37 | border-color: #eee;
38 |
39 | width: 60% !important;
40 | }
41 | .ant-input-group-addon {
42 | background: #fff;
43 | border: none;
44 | }
45 |
46 | .ant-input-wrapper {
47 | display: flex;
48 | justify-content: space-between;
49 | align-items: center;
50 | }
51 | }
52 | .ant-tabs-bar {
53 | margin-bottom: 0;
54 | }
55 |
--------------------------------------------------------------------------------
/panel/routes/_scroll.scss:
--------------------------------------------------------------------------------
1 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
2 | ::-webkit-scrollbar {
3 | display: none;
4 | }
5 |
6 | /*定义滚动条轨道 内阴影+圆角*/
7 | ::-webkit-scrollbar-track {
8 | display: none;
9 | }
10 |
11 | /*定义滑块 内阴影+圆角*/
12 | ::-webkit-scrollbar-thumb {
13 | display: none;
14 |
15 | &:hover {
16 | background: $c-primary;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/panel/routes/index.js:
--------------------------------------------------------------------------------
1 | import pluginCall from 'sketch-module-web-view/client';
2 | import _ from 'lodash';
3 | import { connect } from 'dva';
4 | import { Component } from 'react';
5 | import { Tabs, Button, message } from 'antd';
6 | import { Copyright, Switchbox } from '../components';
7 | import MapSelection from './mapSelection';
8 | import Tab from './Tab';
9 | import Any from './Any';
10 | import Text from './Text';
11 | import Shape from './Shape';
12 | import Symbol from './Symbol';
13 | import style from './index.scss';
14 |
15 | const TabPane = Tabs.TabPane;
16 |
17 | class WebView extends Component {
18 | FootBar = () => (
19 |
20 |
21 |
22 |
25 |
28 |
29 |
30 |
31 | );
32 |
33 | ResetBtn = () => (
34 |
37 | );
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | onReset = () => {
65 | this.props.reset();
66 | message.success(`Reset Success`);
67 | };
68 |
69 | onTabsChange = activeKey => this.props.update({ page: activeKey });
70 |
71 | onGetBtnClick = () => {
72 | pluginCall('getSelection');
73 | const json = localStorage.getItem('selection');
74 | const data = MapSelection(JSON.parse(json));
75 | this.props.update(data);
76 | this.props.update({ checkAll: false, checkedList: [data.type] }, 'layers');
77 | if (data.name) {
78 | message.success(`Select: ${data.name}`);
79 | } else {
80 | message.error(`Select a target first`);
81 | }
82 | };
83 |
84 | onSelectBtnClick = () => {
85 | const page = this.props.config.page;
86 | const Props = {
87 | Name: ['name', 'id'],
88 | Rect: ['x', 'y', 'width', 'height'],
89 | Prototyping: ['text', 'alignment', 'lineSpacing', 'fixedWidth'],
90 | Style: ['fillColor', 'fillType', 'borderColor', 'borderThickness'],
91 | Symbol: ['symbolId'],
92 | };
93 | const Page = {
94 | Any: {
95 | types: this.props.layers.checkedList,
96 | props: ['Name', 'Rect'],
97 | },
98 | Text: {
99 | types: ['Text'],
100 | props: ['Name', 'Rect', 'Prototyping'],
101 | },
102 | Shape: {
103 | types: ['Shape'],
104 | props: ['Name', 'Rect', 'Style'],
105 | },
106 | Symbol: {
107 | types: ['SymbolInstance', 'SymbolInstance'],
108 | props: ['Name', 'Rect', 'Symbol'],
109 | },
110 | };
111 |
112 | // Map props
113 | const propsType = Page[page].props;
114 | const typesArray = _.compact(Page[this.props.config.page].types);
115 | let pagePropsType = {};
116 | let pageProps = {};
117 | let types = {};
118 | let err = false;
119 |
120 | _.forEach(propsType, props => {
121 | pagePropsType[props] = Props[props];
122 | });
123 |
124 | _.forEach(pagePropsType, (props, key) => {
125 | _.forEach(props, type => {
126 | if (this.props.config[`${type}Checked`]) {
127 | if (this.props.config[type] !== '') {
128 | if (!pageProps[key]) pageProps[key] = {};
129 | pageProps[key][type] = this.props.config[type];
130 | if (type === 'text') pageProps.Prototyping.textReg = this.props.config.textRegSwitch;
131 | } else {
132 | err = type;
133 | }
134 | }
135 | });
136 | });
137 |
138 | if (err) return message.error(`${err} need to set`);
139 |
140 | _.forEach(typesArray, type => (types[type] = true));
141 |
142 | const data = {
143 | types,
144 | page,
145 | global: this.props.config.globalSwitch,
146 | config: pageProps,
147 | };
148 |
149 | console.log(data);
150 |
151 | if (
152 | Object.keys(data.types).length === 0 ||
153 | (page !== 'Any' && Object.keys(data.config).length === 0)
154 | ) {
155 | return message.error('Config is empty');
156 | }
157 |
158 | pluginCall('select', JSON.stringify(data));
159 | };
160 | }
161 |
162 | export default connect(Tab.State, Tab.Func)(WebView);
163 |
--------------------------------------------------------------------------------
/panel/routes/index.scss:
--------------------------------------------------------------------------------
1 | $c-primary: #32d1ff;
2 | @import 'scroll';
3 | :global {
4 | @import 'reset';
5 | }
6 | .banner {
7 | width: 100%;
8 | height: 150px;
9 | background-repeat: no-repeat;
10 | background-size: auto 90%;
11 | background-position: top center;
12 | background-color: $c-primary;
13 | padding: 1rem;
14 | }
15 |
16 | .view .container {
17 | width: 100vw;
18 | height: calc(100vh - 320px);
19 | padding: 0.5rem 1rem 1rem;
20 | overflow-x: hidden;
21 | overflow-y: auto;
22 | border: none !important;
23 | background: transparent;
24 | }
25 | .btnGroup {
26 | display: flex;
27 | margin-top: 1rem;
28 | }
29 | .getBtn,
30 | .submitBtn {
31 | flex: 1;
32 |
33 | border-radius: 3rem;
34 | }
35 | .getBtn {
36 | margin-right: 1rem;
37 | }
38 | .submitBtn {
39 | font-weight: 600;
40 | border: none;
41 | background: linear-gradient(
42 | 45deg,
43 | $c-primary,
44 | lighten($c-primary, 16%)
45 | ) !important;
46 | box-shadow: 0 4px 24px rgba($c-primary, 0.25);
47 | }
48 | .resetBtn {
49 | border-radius: 2rem;
50 | margin: 0 auto;
51 | float: right;
52 | font-size: 0.8rem;
53 | border: none;
54 | color: #fff !important;
55 | background: rgba(#fff, 0.2) !important;
56 | }
57 | .footbar {
58 | position: fixed;
59 | bottom: 0;
60 | left: 0;
61 | width: 100vw;
62 | padding: 1rem;
63 | background: #fff;
64 | box-shadow: 0 -4px 16px rgba(#000, 0.05);
65 | }
66 | .split {
67 | padding-bottom: 0.5rem;
68 | margin-bottom: 0.5rem;
69 | border-bottom: 1px solid #eee;
70 | }
71 |
--------------------------------------------------------------------------------
/panel/routes/mapSelection.js:
--------------------------------------------------------------------------------
1 | export default data => {
2 | return {
3 | ...map(data, 'type'),
4 | ...map(data, 'name'),
5 | ...map(data, 'id'),
6 | ...mapFrame(data, 'x'),
7 | ...mapFrame(data, 'y'),
8 | ...mapFrame(data, 'width'),
9 | ...mapFrame(data, 'height'),
10 | ...map(data, 'text'),
11 | ...map(data, 'alignment'),
12 | ...map(data, 'lineSpacing'),
13 | ...map(data, 'fixedWidth'),
14 | ...mapStyle(data, 'fillColor', 'fills', 'color'),
15 | ...mapStyle(data, 'fillType', 'fills', 'fill'),
16 | ...mapStyle(data, 'borderColor', 'borders', 'color'),
17 | ...mapStyle(data, 'borderThickness', 'borders', 'thickness'),
18 | ...map(data, 'symbolId'),
19 | };
20 | };
21 |
22 | function map(data, key, dataKey) {
23 | try {
24 | return {
25 | [key]: data[dataKey || key],
26 | };
27 | } catch (e) {}
28 | }
29 |
30 | function mapFrame(data, key) {
31 | try {
32 | return {
33 | [key]: data.frame[key],
34 | };
35 | } catch (e) {}
36 | }
37 |
38 | function mapStyle(data, key, type, dataKey) {
39 | try {
40 | return {
41 | [key]: data.style[type][0][dataKey],
42 | };
43 | } catch (e) {}
44 | }
45 |
--------------------------------------------------------------------------------
/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/public/banner.png
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canisminor1990/sketch-select/7d00f48db0f235b48670e4b575a2db3379507c94/public/icon.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import sketch from 'sketch/dom';
2 | import UI from 'sketch/ui';
3 | import WebUI from 'sketch-module-web-view';
4 | import { mapLayers, selectLayers, hex2NSColor, openURL } from './utils';
5 |
6 | const isDev = process.env.NODE_ENV === 'development';
7 | const Panel = isDev ? 'http://localhost:8000' : 'index.html';
8 |
9 | export default context => {
10 | const document = sketch.getSelectedDocument();
11 | const panelID = 'sketch-select.panel';
12 |
13 | const panelUI = new WebUI(context, Panel, {
14 | identifier: panelID,
15 | x: 0,
16 | y: 0,
17 | width: 340,
18 | height: 624,
19 | title: 'Sketch Select',
20 | onlyShowCloseButton: true,
21 | background: hex2NSColor('32d1ff'),
22 | hideTitleBar: false,
23 | shouldKeepAround: true,
24 | resizable: false,
25 | handlers: {
26 | select: callback => {
27 | const config = JSON.parse(callback);
28 | let Target;
29 | console.log(config);
30 | if (config.global) {
31 | Target = document.selectedPage;
32 | } else {
33 | Target = document.selectedLayers.layers[0];
34 | Target = searchArtboard(Target);
35 | if (!Target) return UI.alert('🖱 Select', `Select target-artboard first ~`);
36 | }
37 | const AllLayers = mapLayers(Target, config);
38 | if (AllLayers.length > 0) {
39 | document.selectedLayers.clear();
40 | selectLayers(AllLayers);
41 | UI.message(`🖱 Selected ${AllLayers.length} Layers!`);
42 | } else {
43 | UI.alert('🖱 Select', `Nothing to select ...`);
44 | }
45 | },
46 | getSelection() {
47 | const selection = document.selectedLayers.layers[0];
48 | const data = JSON.stringify(selection);
49 | UI.message(`🖱 Select: ${selection.name} (${selection.type})`);
50 | console.log(data);
51 | panelUI.eval(`getSelection(${data})`);
52 | },
53 | openWeb: url => openURL(url),
54 | },
55 | });
56 | };
57 | function searchArtboard(target) {
58 | if (!target) return false;
59 | if (target.type === 'Artboard') return target;
60 | if (target.parent) return searchArtboard(target.parent);
61 | return false;
62 | }
63 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Sketch Select",
3 | "identifier": "cm.sketch.select",
4 | "compatibleVersion": 3,
5 | "bundleVersion": 1,
6 | "icon": "icon.png",
7 | "commands": [{
8 | "name": "🖱 Select",
9 | "script": "index.js",
10 | "identifier": "sketch-select",
11 | "shortcut": "command shift f"
12 | }],
13 | "menu": {
14 | "title": "Select",
15 | "isRoot": true,
16 | "items": [
17 | "sketch-select"
18 | ]
19 | }
20 | }
--------------------------------------------------------------------------------
/src/utils/hex2NSColor.js:
--------------------------------------------------------------------------------
1 | export default hex => {
2 | const r = parseInt(hex.substring(0, 2), 16) / 255;
3 | const g = parseInt(hex.substring(2, 4), 16) / 255;
4 | const b = parseInt(hex.substring(4, 6), 16) / 255;
5 | const a = 1;
6 | return NSColor.colorWithRed_green_blue_alpha(r, g, b, a);
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as mapLayers } from './mapLayers';
2 | export { default as selectLayers } from './selectLayers';
3 | export { default as hex2NSColor } from './hex2NSColor';
4 | export { default as openURL } from './openURL';
5 |
--------------------------------------------------------------------------------
/src/utils/mapLayers.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default (page, opt = {}) => {
4 | const configs = _.assign(
5 | {
6 | Artboard: false,
7 | Page: false,
8 | Group: false,
9 | Text: false,
10 | Shape: false,
11 | Image: false,
12 | SymbolInstance: false,
13 | SymbolMaster: false,
14 | SymbolOverride: false,
15 | },
16 | opt.types
17 | );
18 | const All = [];
19 | const Layers = [];
20 | let Filter = [];
21 | const mapLayers = layers => {
22 | _.forEach(layers, layer => {
23 | All.push(layer);
24 | if (!layer.layers) {
25 | Layers.push(layer);
26 | } else {
27 | mapLayers(layer.layers);
28 | }
29 | });
30 | };
31 |
32 | mapLayers(page.layers);
33 |
34 | /*
35 | const Props = {
36 | Name: ['name', 'id'],
37 | Rect: ['x', 'y', 'width', 'height'],
38 | Prototyping: ['text', 'alignment', 'lineSpacing', 'fixedWidth'],
39 | Style: ['fillColor', 'fillType', 'borderColor', 'borderThickness'],
40 | Symbol: ['symbolId'],
41 | };
42 | */
43 |
44 | _.forEach(All, layer => {
45 | let save = true;
46 | if (!configs[layer.type]) save = false;
47 |
48 | if (save && opt.config.Name) {
49 | _.forEach(opt.config.Name, (value, key) => {
50 | if (layer[key] !== value) save = false;
51 | });
52 | }
53 |
54 | if (save && opt.config.Rect) {
55 | _.forEach(opt.config.Rect, (value, key) => {
56 | if (layer.frame[key] !== value) save = false;
57 | });
58 | }
59 |
60 | if (save && opt.config.Symbol) {
61 | _.forEach(opt.config.Symbol, (value, key) => {
62 | if (layer[key] !== value) save = false;
63 | });
64 | }
65 |
66 | if (save && opt.config.Prototyping) {
67 | _.forEach(opt.config.Prototyping, (value, key) => {
68 | if (key === 'text') {
69 | if (opt.config.Prototyping.textReg) {
70 | const reg = new RegExp(value);
71 | if (!reg.test(layer.text)) save = false;
72 | } else {
73 | if (layer.text !== value) save = false;
74 | }
75 | } else {
76 | if (layer[key] && layer[key] !== value) save = false;
77 | }
78 | });
79 | }
80 |
81 | if (save && opt.config.Style) {
82 | const Style = opt.config.Style;
83 | const fill = layer.style.fills[0];
84 | const border = layer.style.borders[0];
85 | if (save && Style.fillColor) {
86 | if (!fill) save = false;
87 | if (save && fill.color !== Style.fillColor) save = false;
88 | }
89 | if (save && Style.fillType) {
90 | if (!fill) save = false;
91 | if (save && fill.fill !== Style.fillType) save = false;
92 | }
93 | if (save && Style.borderColor) {
94 | if (!border) save = false;
95 | if (save && border.color !== Style.borderColor) save = false;
96 | }
97 | if (save && Style.borderThickness) {
98 | if (!border) save = false;
99 | if (save && border.thickness !== Style.borderThickness) save = false;
100 | }
101 | }
102 |
103 | if (save) Filter.push(layer);
104 | });
105 |
106 | console.log('Filter', JSON.stringify(Filter));
107 |
108 | return Filter;
109 | };
110 |
--------------------------------------------------------------------------------
/src/utils/openURL.js:
--------------------------------------------------------------------------------
1 | export default url => NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url));
2 |
--------------------------------------------------------------------------------
/src/utils/selectLayers.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default layers => {
4 | try {
5 | _.forEach(layers, layer => (layer.selected = true));
6 | } catch (e) {
7 | console.log(e);
8 | }
9 | };
10 |
--------------------------------------------------------------------------------