@pageboard/html 0.15.14 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ exports.consent_form = {
2
2
  priority: 11,
3
3
  title: 'Consent',
4
4
  icon: '<i class="handshake icon"></i>',
5
- group: "block",
5
+ group: "block form",
6
6
  menu: 'form',
7
7
  properties: {
8
8
  transient: {
@@ -17,44 +17,92 @@ exports.consent_form = {
17
17
  nodes: "block+"
18
18
  },
19
19
  html: `<form is="element-consent" class="ui form" data-transient="[transient]">
20
- <x[transient|alt:template:div|at:-] block-content="content"></x[transient|alt:template:div|at:-]>
20
+ <template block-content="content"></template>
21
+ <div class="view"></div>
21
22
  </form>`,
22
23
  scripts: ['../ui/storage.js', '../ui/consent.js'],
23
24
  stylesheets: ['../ui/consent.css']
24
25
  };
25
26
 
27
+ const consents = [];
28
+
29
+ exports.input_radio_consent = {
30
+ title: 'Consent custom',
31
+ icon: '<i class="hand scissors icon"></i>',
32
+ menu: "form",
33
+ group: "block",
34
+ context: 'consent_form//',
35
+ properties: {
36
+ value: {
37
+ title: 'Consent',
38
+ anyOf: [{
39
+ const: 'custom',
40
+ title: 'Custom'
41
+ }, {
42
+ const: 'yes',
43
+ title: 'All'
44
+ }, {
45
+ const: 'no',
46
+ title: 'None'
47
+ }]
48
+ }
49
+ },
50
+ contents: {
51
+ id: 'label',
52
+ nodes: 'inline*'
53
+ },
54
+ html: `<div class="field">
55
+ <div class="ui radio checkbox">
56
+ <input type="radio" name="consent" value="[value]" id="for-consent-[value]" />
57
+ <label block-content="label" for="for-consent-[value]">Custom</label>
58
+ </div>
59
+ </div>`
60
+ };
61
+
26
62
  exports.input_radio_yes = {
27
- title: 'Yes',
63
+ title: 'Consent yes',
28
64
  icon: '<i class="thumbs up icon"></i>',
29
65
  menu: "form",
30
66
  group: "block",
31
67
  context: 'consent_form//',
68
+ properties: {
69
+ consent: {
70
+ title: 'Consent',
71
+ anyOf: consents
72
+ }
73
+ },
32
74
  contents: {
33
75
  id: 'label',
34
76
  nodes: 'inline*'
35
77
  },
36
78
  html: `<div class="field">
37
79
  <div class="ui radio checkbox">
38
- <input type="radio" name="consent" value="yes" id="for-consent-yes" />
39
- <label block-content="label" for="for-consent-yes">Yes</label>
80
+ <input type="radio" name="consent.[consent]" value="yes" id="for-consent-yes-[consent]" />
81
+ <label block-content="label" for="for-consent-yes-[consent]">Yes</label>
40
82
  </div>
41
83
  </div>`
42
84
  };
43
85
 
44
86
  exports.input_radio_no = {
45
- title: 'No',
87
+ title: 'Consent no',
46
88
  icon: '<i class="thumbs down icon"></i>',
47
89
  menu: "form",
48
90
  group: "block",
49
91
  context: 'consent_form//',
92
+ properties: {
93
+ consent: {
94
+ title: 'Consent',
95
+ anyOf: consents
96
+ }
97
+ },
50
98
  contents: {
51
99
  id: 'label',
52
100
  nodes: 'inline*'
53
101
  },
54
102
  html: `<div class="field">
55
103
  <div class="ui radio checkbox">
56
- <input type="radio" name="consent" value="no" id="for-consent-no" />
57
- <label block-content="label" for="for-consent-no">No</label>
104
+ <input type="radio" name="consent.[consent]" value="no" id="for-consent-no-[consent]" />
105
+ <label block-content="label" for="for-consent-no-[consent]">No</label>
58
106
  </div>
59
107
  </div>`
60
108
  };
package/elements/embed.js CHANGED
@@ -55,3 +55,8 @@ exports.embed = {
55
55
  '../ui/linkable.css'
56
56
  ]
57
57
  };
58
+
59
+ exports.input_radio_yes.properties.consent.anyOf.push({
60
+ title: "Embeds",
61
+ const: "embed"
62
+ });
package/elements/form.js CHANGED
@@ -31,15 +31,20 @@ exports.query_form = {
31
31
  }
32
32
  },
33
33
  redirection: {
34
- title: 'Target Address',
34
+ title: 'Target',
35
35
  type: 'object',
36
36
  properties: {
37
37
  url: {
38
- title: 'Page',
38
+ title: 'Address',
39
39
  nullable: true,
40
40
  type: "string",
41
- format: "page",
42
- $helper: "page"
41
+ format: 'uri-reference',
42
+ $helper: {
43
+ name: 'href',
44
+ filter: {
45
+ type: ["link", "file", "archive"]
46
+ }
47
+ }
43
48
  },
44
49
  parameters: {
45
50
  title: 'Parameters',
@@ -89,8 +94,7 @@ exports.api_form = {
89
94
  title: 'Hidden',
90
95
  description: 'Hidden and disabled\nShown by $query.toggle',
91
96
  type: 'boolean',
92
- default: false,
93
- context: 'template'
97
+ default: false
94
98
  },
95
99
  action: {
96
100
  title: 'Action',
@@ -177,10 +181,10 @@ exports.api_form = {
177
181
  id="[name|else:$id]"
178
182
  action="/@api/form/[$id]"
179
183
  parameters="[action?.request|as:expressions]"
180
- success="[redirection.parameters|as:query]"
181
- badrequest="[badrequest.parameters|as:query]"
182
- unauthorized="[unauthorized.parameters|as:query]"
183
- notfound="[notfound.parameters|as:query]"
184
+ success="[redirection.parameters|as:query|as:null]"
185
+ badrequest="[badrequest.parameters|as:query|as:null]"
186
+ unauthorized="[unauthorized.parameters|as:query|as:null]"
187
+ notfound="[notfound.parameters|as:query|as:null]"
184
188
  class="ui form"></form>`,
185
189
  stylesheets: [
186
190
  '../ui/components/form.css',
package/elements/image.js CHANGED
@@ -128,14 +128,13 @@ exports.image = {
128
128
  }, {
129
129
  id: 'alt',
130
130
  title: 'Alternative Text',
131
- $attr: 'data-alt',
132
131
  $helper: {
133
132
  name: 'describe'
134
133
  }
135
134
  }],
136
135
  html: `<element-image
137
136
  class="[display.fit|or:none] [display.horizontal?] [display.vertical?]"
138
- data-src="[url]"
137
+ data-src="[url]" data-alt="[$content.alt]"
139
138
  data-crop="[crop.x|or:50];[crop.y|or:50];[crop.width|or:100];[crop.height|or:100];[crop.zoom|or:100]"
140
139
  >
141
140
  <div block-content="legend"></div>
@@ -270,6 +270,6 @@ exports.notranslate = {
270
270
  group: "inline nolink",
271
271
  icon: '<i class="large translate icon"></i>',
272
272
  tag: 'span[translate="no"]',
273
- html: '<span translate="no"></span>'
273
+ html: '<span translate="no" spellcheck="false"></span>'
274
274
  };
275
275
 
@@ -13,8 +13,7 @@ exports.input_button = {
13
13
  title: 'Type',
14
14
  default: 'submit',
15
15
  anyOf: [{
16
- title: 'Submit',
17
- const: 'submit'
16
+ const: 'submit' // deprecated
18
17
  }, {
19
18
  title: 'Reset',
20
19
  const: 'reset'
@@ -90,6 +89,93 @@ exports.input_button = {
90
89
  ]
91
90
  };
92
91
 
92
+ exports.input_submit = {
93
+ title: 'Submit',
94
+ icon: '<i class="icons"><i class="hand pointer icon"></i><i class="corner write icon"></i></i>',
95
+ menu: "form",
96
+ group: "block input_field",
97
+ context: 'form//',
98
+ contents: {
99
+ nodes: "inline*",
100
+ marks: "nolink"
101
+ },
102
+ properties: {
103
+ action: {
104
+ title: 'Action',
105
+ type: 'string',
106
+ format: 'pathname',
107
+ nullable: true,
108
+ $helper: {
109
+ name: 'datalist',
110
+ url: '/@api/block/search?type=api_form&limit=20&data.name:not&order=data.name',
111
+ value: '/@api/form/[data.name]',
112
+ title: '[$services.[data.action.method|enc:path].title] / [data.name|else:id]'
113
+ }
114
+ },
115
+ name: {
116
+ title: "Name",
117
+ description: "The form object key",
118
+ type: "string",
119
+ format: "singleline"
120
+ },
121
+ value: {
122
+ title: "Default value",
123
+ nullable: true,
124
+ type: "string",
125
+ format: "singleline"
126
+ },
127
+ form: {
128
+ title: 'Target form',
129
+ type: 'string',
130
+ format: 'name',
131
+ nullable: true,
132
+ $filter: {
133
+ name: 'action',
134
+ action: 'write'
135
+ }
136
+ },
137
+ disabled: {
138
+ title: 'Disabled',
139
+ type: 'boolean',
140
+ default: false
141
+ },
142
+ full: {
143
+ title: 'Fluid',
144
+ type: 'boolean',
145
+ default: false
146
+ },
147
+ icon: {
148
+ title: 'Icon',
149
+ type: 'boolean',
150
+ default: false
151
+ },
152
+ compact: {
153
+ title: 'Compact',
154
+ type: 'boolean',
155
+ default: false
156
+ },
157
+ float: {
158
+ title: 'Float',
159
+ anyOf: [{
160
+ type: 'null',
161
+ title: 'No'
162
+ }, {
163
+ const: 'left',
164
+ title: 'Left'
165
+ }, {
166
+ const: 'right',
167
+ title: 'Right'
168
+ }],
169
+ default: null
170
+ }
171
+ },
172
+ html: '<button type="submit" formaction="[action]" form="[form]" disabled="[disabled]" class="ui [full|alt:fluid:] [icon] [compact] [float|post:%20floated] button" name="[name]" value="[value]">Submit</button>',
173
+ stylesheets: [
174
+ '../ui/components/button.css',
175
+ '../ui/button.css'
176
+ ]
177
+ };
178
+
93
179
  exports.input_fields = {
94
180
  title: 'Input Fields',
95
181
  icon: '<i class="icon columns"></i>',
@@ -206,8 +292,8 @@ exports.input_text = {
206
292
  nodes: 'inline*'
207
293
  },
208
294
  patterns: {
209
- tel: /^(\(\d+\))? *\d+([ .-]?\d+)*$/.source,
210
- email: /^[\w.!#$%&'*+/=?^`{|}~-]+@\w(?:[\w-]{0,61}\w)?(?:\.\w(?:[\w-]{0,61}\w)?)*$/.source
295
+ tel: /^(\(\d+\))? *\d+([ .\-]?\d+)*$/v.source,
296
+ email: /[\w.!#$%&'*+\/=?^`\{\|\}~\-]+@\w(?:[\w\-]{0,61}\w)?(?:\.\w(?:[\w\-]{0,61}\w)?)*/v.source
211
297
  },
212
298
  html: `<div class="[width|as:colnums|post: wide] field [type|if:eq:hidden]">
213
299
  <label block-content="label">Label</label>
package/elements/link.js CHANGED
@@ -29,7 +29,9 @@ exports.link = {
29
29
  nullable: true,
30
30
  $helper: {
31
31
  name: 'datalist',
32
- url: '/@api/translate/languages'
32
+ url: '/@api/translate/languages',
33
+ value: '[data.lang]',
34
+ title: '[content.]'
33
35
  }
34
36
  },
35
37
  id: {
@@ -75,7 +77,9 @@ exports.link_button = {
75
77
  nullable: true,
76
78
  $helper: {
77
79
  name: 'datalist',
78
- url: '/@api/translate/languages'
80
+ url: '/@api/translate/languages',
81
+ value: '[data.lang]',
82
+ title: '[content.]'
79
83
  }
80
84
  },
81
85
  full: {
package/elements/menu.js CHANGED
@@ -102,7 +102,9 @@ exports.menu_item_link = {
102
102
  nullable: true,
103
103
  $helper: {
104
104
  name: 'datalist',
105
- url: '/@api/translate/languages'
105
+ url: '/@api/translate/languages',
106
+ value: '[data.lang]',
107
+ title: '[content.]'
106
108
  }
107
109
  },
108
110
  labeled: {
package/elements/page.js CHANGED
@@ -1,20 +1,16 @@
1
- exports.page.stylesheets = [
2
- ...exports.page.stylesheets,
1
+ exports.page.stylesheets.push(
3
2
  '../ui/components/reset.css',
4
3
  '../ui/site.css',
5
4
  '../ui/page.css',
6
5
  '../ui/transition.css'
7
- ];
6
+ );
8
7
 
9
- exports.page.resources = {
10
- reset: '../ui/components/reset.css',
11
- site: '../ui/site.css'
12
- };
8
+ exports.page.resources.reset = '../ui/components/reset.css';
9
+ exports.page.resources.site = '../ui/site.css';
13
10
 
14
- exports.page.scripts = [
15
- ...exports.page.scripts,
11
+ exports.page.scripts.push(
16
12
  '../ui/transition.js'
17
- ];
13
+ );
18
14
 
19
15
  exports.page.properties.transition = {
20
16
  title: 'Transition',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pageboard/html",
3
- "version": "0.15.14",
3
+ "version": "0.16.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -16,14 +16,14 @@
16
16
  "dependencies": {},
17
17
  "devDependencies": {
18
18
  "nouislider": "^15.8.1",
19
- "postinstall": "^0.10.3"
19
+ "postinstall": "^0.11.0"
20
20
  },
21
- "postinstall": {},
22
- "prepare": {
23
- "nouislider/dist/nouislider.js": "copy lib/",
24
- "nouislider/dist/nouislider.css": "copy lib/"
21
+ "postinstall": {
22
+ "nouislider/dist/nouislider.js": "link lib/",
23
+ "nouislider/dist/nouislider.css": "link lib/"
25
24
  },
26
25
  "pageboard": {
26
+ "version": "^0.16",
27
27
  "priority": -10,
28
28
  "directories": [
29
29
  "elements",
package/ui/consent.css CHANGED
@@ -13,9 +13,16 @@ footer [block-type="consent_form"][data-transient="true"] {
13
13
  [block-type="consent_form"][data-transient="true"]:not(.visible) {
14
14
  display:none;
15
15
  }
16
- .visible[block-type="consent_form"][data-transient="true"] > template {
17
- display:block !important;
18
- position: relative;
16
+
17
+ [block-type="consent_form"] > [block-content="content"] {
18
+ display:none;
19
+ }
20
+ [block-type="consent_form"] > .view {
21
+ position:relative;
22
+ }
23
+ [contenteditable] [block-type="consent_form"] > [block-content="content"] {
24
+ display:block;
25
+ min-height:1em;
19
26
  }
20
27
 
21
28
  [contenteditable] [block-focused="last"] > .form[block-type="consent_form"][data-transient="true"],
package/ui/consent.js CHANGED
@@ -3,51 +3,62 @@ class HTMLElementConsent extends Page.create(HTMLFormElement) {
3
3
  dataTransient: false
4
4
  };
5
5
 
6
- static ask() {
7
- this.waiting = false;
6
+ static explicits = new Set();
7
+
8
+ static ask(state, consent) {
8
9
  let tacit = true;
9
- for (const node of document.querySelectorAll('[block-type="consent_form"]')) {
10
+ const forms = document.querySelectorAll('[block-type="consent_form"]');
11
+ const consents = state.scope.storage.all();
12
+ for (const node of forms) {
13
+ window.HTMLElementForm.prototype.fill.call(node, consents);
10
14
  node.classList.add('visible');
11
- tacit = false;
15
+ tacit = consent && !node.querySelector(`[name="${consent}"]`) || false;
12
16
  }
13
- return !tacit;
17
+ if (!tacit) this.explicits.add(consent);
18
+ return tacit ? "yes" : null;
14
19
  }
15
20
  setup(state) {
16
21
  if (state.scope.$write) return;
22
+ this.constructor.explicits = new Set();
23
+ const view = this.ownView;
24
+ view.textContent = '';
25
+ const tmpl = this.ownTpl.prerender();
26
+ view.appendChild(tmpl.content.cloneNode(true));
27
+ state.chain('consent', this);
28
+ }
29
+ chainConsent(state) {
17
30
  if (this.options.transient) {
18
- const tmpl = this.ownTpl.prerender();
19
- if (tmpl.content && tmpl.children.length == 0) {
20
- tmpl.appendChild(tmpl.content);
21
- }
31
+ this.classList.remove('visible');
32
+ } else {
33
+ window.HTMLElementForm.prototype.fill.call(this, state.scope.storage.all());
22
34
  }
23
- state.consent(this);
24
35
  }
25
- chainConsent(state) {
26
- window.HTMLElementForm.prototype.fill.call(this, {
27
- consent: state.scope.$consent
28
- });
29
- if (this.options.transient) this.classList.remove('visible');
36
+ handleChange(e, state) {
37
+ if (e.type == "submit" || !this.elements.find(item => item.type == "submit")) {
38
+ this.handleSubmit(e, state);
39
+ }
30
40
  }
31
41
  handleSubmit(e, state) {
32
42
  if (e.type == "submit") e.preventDefault();
33
43
  if (state.scope.$write) return;
34
- const fd = window.HTMLElementForm.prototype.read.call(this);
35
- const consent = fd.consent;
36
- if (consent == null) {
44
+ const consents = window.HTMLElementForm.prototype.read.call(this);
45
+ const list = Array.from(this.constructor.explicits);
46
+ const def = consents.consent;
47
+ for (const consent of list) {
48
+ if (def != "custom") consents[consent] = def;
49
+ }
50
+ if (list.some(c => consents[c] == null)) {
51
+ // not all explicit consents have been answered
37
52
  return;
38
53
  }
39
- state.scope.storage.set('consent', consent);
40
- state.scope.$consent = consent;
54
+ for (const [key, val] of Object.entries(consents)) {
55
+ state.scope.storage.set(key, val);
56
+ }
41
57
  state.copy().runChain('consent');
42
58
  }
43
- handleChange(e, state) {
44
- this.handleSubmit(e, state);
45
- }
46
59
  patch(state) {
47
60
  if (state.scope.$write) return;
48
- if (this.options.transient) {
49
- this.ownTpl.prerender();
50
- }
61
+ this.ownTpl.prerender();
51
62
  }
52
63
  get ownTpl() {
53
64
  return this.children.find(
@@ -59,31 +70,17 @@ class HTMLElementConsent extends Page.create(HTMLFormElement) {
59
70
  }
60
71
  }
61
72
 
62
- Page.constructor.prototype.consent = function (fn) {
63
- const initial = this.scope.$consent === undefined;
64
- let consent = this.scope.storage.get('consent');
65
- if (consent == null && initial) consent = undefined;
66
- this.scope.$consent = consent;
67
- this.chain('consent', fn);
68
- if (consent === undefined) {
69
- HTMLElementConsent.waiting = true;
70
- } else if (consent === null) {
71
- // setup finished but no consent is done yet, ask consent
72
- this.reconsent();
73
+ Page.constructor.prototype.consent = function (listener, ask) {
74
+ const { consent } = listener.constructor;
75
+ if (!consent) {
76
+ console.warn("Expected a static consent field", listener);
77
+ return;
73
78
  }
74
- };
75
-
76
- Page.constructor.prototype.reconsent = function (fn) {
77
- if (fn) this.consent(fn);
78
- const consent = this.scope.$consent;
79
- let asking = false;
80
- if (consent != "yes") {
81
- asking = HTMLElementConsent.ask();
79
+ const cur = this.scope.storage.get(consent);
80
+ if (cur == null || ask) {
81
+ this.scope.storage.set(consent, HTMLElementConsent.ask(this, consent));
82
82
  }
83
- if (!asking) {
84
- if (consent == null) this.scope.$consent = "yes";
85
- }
86
- return asking;
83
+ this.chain('consent', listener);
87
84
  };
88
85
 
89
86
  Page.define(`element-consent`, HTMLElementConsent, 'form');
@@ -91,12 +88,7 @@ Page.define(`element-consent`, HTMLElementConsent, 'form');
91
88
 
92
89
  Page.paint(state => {
93
90
  state.finish(() => {
94
- let run = true;
95
- if (HTMLElementConsent.waiting) {
96
- if (state.reconsent()) run = false;
97
- }
98
- if (run) {
99
- // do not change current state stage
91
+ if (!HTMLElementConsent.explicits.size) {
100
92
  state.copy().runChain('consent');
101
93
  }
102
94
  });
package/ui/embed.js CHANGED
@@ -1,4 +1,5 @@
1
1
  class HTMLElementEmbed extends Page.Element {
2
+ static consent = "consent.embed";
2
3
  static defaults = {
3
4
  src: null,
4
5
  query: null,
@@ -11,7 +12,7 @@ class HTMLElementEmbed extends Page.Element {
11
12
  state.consent(this);
12
13
  }
13
14
  get currentSrc() {
14
- return this.querySelector('iframe')?.src ?? "about:blank";
15
+ return this.querySelector('iframe')?.getAttribute('src');
15
16
  }
16
17
  patch(state) {
17
18
  const { src } = this.options;
@@ -21,7 +22,7 @@ class HTMLElementEmbed extends Page.Element {
21
22
  if (width && height) this.style.paddingBottom = `calc(${height} / ${width} * 100%)`;
22
23
  }
23
24
  consent(state) {
24
- const consent = state.scope.$consent;
25
+ const consent = state.scope.storage.get(this.constructor.consent);
25
26
  this.classList.toggle('denied', consent == "no");
26
27
  this.classList.toggle('waiting', consent == null);
27
28
 
@@ -57,7 +58,9 @@ class HTMLElementEmbed extends Page.Element {
57
58
  }
58
59
  }
59
60
  captureClick(e, state) {
60
- if (this.matches('.denied')) state.reconsent();
61
+ if (this.matches('.denied')) {
62
+ state.consent(this, true);
63
+ }
61
64
  }
62
65
  captureLoad() {
63
66
  this.classList.remove('loading');
package/ui/form.js CHANGED
@@ -98,16 +98,19 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
98
98
  }
99
99
  }
100
100
  paint(state) {
101
- // ?submit=<name> for auto-submit
102
- // WORKAROUND use Page instead of state
103
- const name = state.query.submit;
104
- if (!name || name != this.name) return;
105
- // make sure to not resubmit in case of self-redirection
106
- delete state.query.submit;
107
- state.finish(() => {
108
- if (state.status != 200) return;
109
- state.dispatch(this, 'submit');
110
- });
101
+ if (state.scope.$write) return;
102
+ const name = state.query.submit; // explicit auto-submit
103
+ if (name && name == this.name || this.elements.length == 0 && this.action != state.toString()) {
104
+ if (state.scope.$read) {
105
+ console.info("form#paint would auto-submit:", this.action);
106
+ } else {
107
+ delete state.query.submit;
108
+ state.finish(() => {
109
+ if (state.status != 200) return;
110
+ state.dispatch(this, 'submit');
111
+ });
112
+ }
113
+ }
111
114
  }
112
115
  read(withDefaults = false, submitter) {
113
116
  const fd = new FormData(this, submitter);
@@ -322,7 +325,9 @@ class HTMLElementForm extends Page.create(HTMLFormElement) {
322
325
  form.disable();
323
326
  form.classList.add('loading');
324
327
  scope.$response = await state.fetch(
325
- form.method, form.getAttribute('action'), scope.$request
328
+ form.method,
329
+ e.submitter?.getAttribute('formaction') || form.action,
330
+ scope.$request
326
331
  );
327
332
  } catch (err) {
328
333
  scope.$response = err;
@@ -416,8 +421,8 @@ HTMLButtonElement.prototype.fill = HTMLInputElement.prototype.fill = function (v
416
421
  this.checked = true;
417
422
  } else {
418
423
  this.checked = (Array.isArray(val) ? val : [val]).some(str => {
419
- if (str == false && this.value == "") return true;
420
- return str.toString() == this.value;
424
+ if ((str == false || str == null) && this.value == "") return true;
425
+ return (str ?? '').toString() == this.value;
421
426
  });
422
427
  }
423
428
  } else {
package/ui/inlines.css CHANGED
@@ -38,3 +38,6 @@ span.purple {
38
38
  color: purple;
39
39
  }
40
40
 
41
+ [contenteditable] span[translate="no"] {
42
+ text-decoration: dashed underline;
43
+ }