Browse Source

Merge dev

pull/2474/head
Artur Arseniev 7 years ago
parent
commit
4e4b66ec46
  1. 2
      dist/css/grapes.min.css
  2. 1
      src/commands/index.js
  3. 7
      src/dom_components/model/ComponentTextNode.js
  4. 4
      src/dom_components/view/ComponentTextView.js
  5. 6
      src/dom_components/view/ComponentView.js
  6. 198
      src/i18n/locale/fr.js
  7. 30
      src/rich_text_editor/index.js
  8. 76
      src/rich_text_editor/model/RichTextEditor.js
  9. 9
      src/styles/scss/_gjs_rte.scss
  10. 31
      test/specs/dom_components/model/Component.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

1
src/commands/index.js

@ -129,6 +129,7 @@ export default () => {
const defComOptions = { preserveSelected: 1 };
const modes = ['absolute', 'translate'];
const hideTlb = () => em.stopDefault(defComOptions);
selAll.forEach(sel => sel.trigger('disable'));
// Dirty patch to prevent parent selection on drop (in absolute mode)
em.set('_cmpDrag', 1);

7
src/dom_components/model/ComponentTextNode.js

@ -10,7 +10,12 @@ export default Component.extend(
},
toHTML() {
return this.get('content');
return this.get('content')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
},
{

4
src/dom_components/view/ComponentTextView.js

@ -48,6 +48,10 @@ export default ComponentView.extend({
this.toggleEvents(1);
},
onDisable() {
this.disableEditing();
},
/**
* Disable element content editing
* @private

6
src/dom_components/view/ComponentView.js

@ -38,6 +38,7 @@ export default Backbone.View.extend({
this.listenTo(model, 'change:content', this.updateContent);
this.listenTo(model, 'change', this.handleChange);
this.listenTo(model, 'active', this.onActive);
this.listenTo(model, 'disable', this.onDisable);
$el.data('model', model);
model.view = this;
this.initClasses();
@ -74,6 +75,11 @@ export default Backbone.View.extend({
*/
onActive() {},
/**
* Callback executed when the `disable` event is triggered on component
*/
onDisable() {},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
this.removed(this._clbObj());

198
src/i18n/locale/fr.js

@ -0,0 +1,198 @@
const traitInputAttr = { placeholder: 'ex. Votre texte ici' };
export default {
assetManager: {
addButton: 'Ajouter image',
inputPlh: 'http://chemin/vers/image.jpg',
modalTitle: 'Sélectionner une image',
uploadTitle: 'Déposez des fichiers ici ou cliquez pour envoyer des fichiers'
},
blockManager: {
labels: {
// 'block-id': 'Identifiant du bloc',
},
categories: {
// 'category-id': 'Identifiant de la catégorie',
}
},
domComponents: {
names: {
'': 'Boîte',
wrapper: 'Corps',
text: 'Texte',
comment: 'Commentaire',
image: 'Image',
video: 'Vidéo',
label: 'Libellé',
link: 'Lien',
map: 'Carte',
tfoot: 'Pied de tableau',
tbody: 'Corps de tableau',
thead: 'En-tête de tableau',
table: 'Tableau',
row: 'Ligne tableau',
cell: 'Cellule tableau'
}
},
deviceManager: {
device: 'Appareil',
devices: {
desktop: 'Ordinateur de bureau',
tablet: 'Tablette',
mobileLandscape: 'Mobile format paysage',
mobilePortrait: 'Mobile format portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Prévisualisation',
fullscreen: 'Plein écran',
'sw-visibility': 'Voir les composants',
'export-template': 'Voir le code',
'open-sm': 'Ouvrir le gestionnaire de style',
'open-tm': 'Paramètres',
'open-layers': 'Ouvrir le gestionnaire de calques',
'open-blocks': 'Ouvrir le gestionnaire de blocs'
}
}
},
selectorManager: {
label: 'Classes',
selected: 'Sélectionné',
emptyState: '- État -',
states: {
hover: 'Survol',
active: 'Clic',
'nth-of-type(2n)': 'Paire/Impaire'
}
},
styleManager: {
empty:
"Veuillez sélectionner un élément avant d'utiliser le gestionnaire de style",
layer: 'Calque',
fileButton: 'Images',
sectors: {
general: 'Général',
layout: 'Disposition',
typography: 'Typographie',
decorations: 'Décorations',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
float: 'Flottant',
display: 'Affichage',
position: 'Position',
top: 'Supérieur',
right: 'Droite',
left: 'Gauche',
bottom: 'Inférieur',
width: 'Largeur',
height: 'Hauteur',
'max-width': 'Largeur max.',
'max-height': 'Hauteur max.',
margin: 'Marge',
'margin-top': 'Marge supérieure',
'margin-right': 'Marge droite',
'margin-left': 'Marge gauche',
'margin-bottom': 'Marge inférieure',
padding: 'Padding',
'padding-top': 'Padding supérieur',
'padding-left': 'Padding gauche',
'padding-right': 'Padding droite',
'padding-bottom': 'Padding inférieur',
'font-family': 'Police de caractères',
'font-size': 'Taille de police',
'font-weight': 'Épaisseur de police',
'letter-spacing': 'Espacement entre les lettres',
color: 'Couleur',
'line-height': 'Espacement des lignes',
'text-align': 'Alignement de texte',
'text-shadow': 'Ombre de texte',
'text-shadow-h': 'Ombre de texte: horizontale',
'text-shadow-v': 'Ombre de texte: verticale',
'text-shadow-blur': 'Flou ombre de texte',
'text-shadow-color': 'Couleur ombre de texte',
'border-top-left': 'Bord supérieur gauche',
'border-top-right': 'Bord supérieur droit',
'border-bottom-left': 'Bord inférieur gauche',
'border-bottom-right': 'Bord inférieur droit',
'border-radius-top-left': 'Bord supérieur arrondi gauche',
'border-radius-top-right': 'Bord supérieur arrondi droit',
'border-radius-bottom-left': 'Bord arrondi inférieur gauche',
'border-radius-bottom-right': 'Bord arrondi inférieur droit',
'border-radius': 'Bord arrondi',
border: 'Bordure',
'border-width': 'Largeur de bordure',
'border-style': 'Style de bordure',
'border-color': 'Couleur de bordure',
'box-shadow': 'Ombre de boîte',
'box-shadow-h': 'Ombre de boîte: horizontale',
'box-shadow-v': 'Ombre de boîte: verticale',
'box-shadow-blur': 'Flou ombre de boîte',
'box-shadow-spread': "Extension d'ombre de boîte",
'box-shadow-color': "Couleur d'ombre de boîte",
'box-shadow-type': "Type d'ombre de boîte",
background: 'Fond',
'background-image': 'Image de fond',
'background-repeat': 'Répéter fond',
'background-position': 'Position du fond',
'background-attachment': 'Plugin de fond',
'background-size': 'Taille du fond',
'background-color': 'Couleur de fond',
transition: 'Transition',
'transition-property': 'Type de transition',
'transition-duration': 'Durée de la transition',
'transition-timing-function': 'Timing transition',
perspective: 'Perspective',
transform: 'Transformation',
'transform-rotate-x': 'Rotation horizontale',
'transform-rotate-y': 'Rotation verticale',
'transform-rotate-z': 'Rotation profondeur',
'transform-scale-x': 'Échelle horizontale',
'transform-scale-y': 'Échelle verticale',
'transform-scale-z': 'Échelle profondeur',
'flex-direction': 'Direction Flex',
'flex-wrap': 'Flex wrap',
'justify-content': 'Ajuster contenu',
'align-items': 'Aligner éléments',
'align-content': 'Aligner contenu',
order: 'Ordre',
'flex-basis': 'Base Flex',
'flex-grow': 'Flex grow',
'flex-shrink': 'Flex shrink',
'align-self': 'Aligner'
}
},
traitManager: {
empty:
'Veuillez sélectionner un élément pour modifier les paramètres de cet élément',
label: 'Paramètres composant',
traits: {
// The core library generates the name by their `name` property
labels: {
id: 'Identifiant',
alt: 'Texte alternatif',
title: 'Titre',
href: 'Source lien'
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'eg. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Cette fenêtre',
_blank: 'Nouvelle fenêtre'
}
}
}
}
};

30
src/rich_text_editor/index.js

@ -109,7 +109,9 @@ export default () => {
const classes = {
actionbar: `${pfx}actionbar`,
button: `${pfx}action`,
active: `${pfx}active`
active: `${pfx}active`,
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`
};
const rte = new RichTextEditor({
el,
@ -165,6 +167,32 @@ export default () => {
* }
* }
* })
* // An example with state
* const isValidAnchor = (rte) => {
* // a utility function to help determine if the selected is a valid anchor node
* const anchor = rte.selection().anchorNode;
* const parentNode = anchor && anchor.parentNode;
* const nextSibling = anchor && anchor.nextSibling;
* return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A')
* }
* rte.add('toggleAnchor', {
* icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
* state: (rte, doc) => {
* if (rte && rte.selection()) {
* // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active
* return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
* } else {
* return btnState.INACTIVE;
* }
* },
* result: (rte, action) => {
* if (isValidAnchor(rte)) {
* rte.exec('unlink');
* } else {
* rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
* }
* }
* })
*/
add(name, action = {}) {
action.name = name;

76
src/rich_text_editor/model/RichTextEditor.js

@ -5,6 +5,20 @@ import { on, off } from 'utils/mixins';
const RTE_KEY = '_rte';
const btnState = {
ACTIVE: 1,
INACTIVE: 0,
DISABLED: -1
};
const isValidAnchor = rte => {
const anchor = rte.selection().anchorNode;
const parentNode = anchor && anchor.parentNode;
const nextSibling = anchor && anchor.nextSibling;
return (
(parentNode && parentNode.nodeName == 'A') ||
(nextSibling && nextSibling.nodeName == 'A')
);
};
const defActions = {
bold: {
name: 'bold',
@ -37,10 +51,15 @@ const defActions = {
style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link'
},
state: (rte, doc) => {
if (rte && rte.selection()) {
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
},
result: rte => {
const anchor = rte.selection().anchorNode;
const nextSibling = anchor && anchor.nextSibling;
if (nextSibling && nextSibling.nodeName == 'A') {
if (isValidAnchor(rte)) {
rte.exec('unlink');
} else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
@ -78,7 +97,9 @@ export default class RichTextEditor {
...{
actionbar: 'actionbar',
button: 'action',
active: 'active'
active: 'active',
disabled: 'disabled',
inactive: 'inactive'
},
...settings.classes
};
@ -114,16 +135,34 @@ export default class RichTextEditor {
this.getActions().forEach(action => {
const btn = action.btn;
const update = action.update;
const active = this.classes.active;
const { active, inactive, disabled } = { ...this.classes };
const state = action.state;
const name = action.name;
const doc = this.doc;
btn.className = btn.className.replace(active, '').trim();
// doc.queryCommandValue(name) != 'false'
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
btn.className = btn.className.replace(inactive, '').trim();
btn.className = btn.className.replace(disabled, '').trim();
// if there is a state function, which depicts the state,
// i.e. `active`, `disabled`, then call it
if (state) {
switch (state(this, doc)) {
case btnState.ACTIVE:
btn.className += ` ${active}`;
break;
case btnState.INACTIVE:
btn.className += ` ${inactive}`;
break;
case btnState.DISABLED:
btn.className += ` ${disabled}`;
break;
}
} else {
// otherwise default to checking if the name command is supported & enabled
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
}
}
update && update(this, action);
});
}
@ -156,11 +195,18 @@ export default class RichTextEditor {
*/
syncActions() {
this.getActions().forEach(action => {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
this.updateActiveActions();
};
if (this.settings.actionbar) {
if (
!action.state ||
(action.state && action.state(this, this.doc) >= 0)
) {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
this.updateActiveActions();
};
}
}
});
}

9
src/styles/scss/_gjs_rte.scss

@ -33,6 +33,13 @@
}
&active {
background-color: $mainDkColor;
background-color: $mainLhColor;
}
&disabled {
color: $mainLhColor;
cursor: not-allowed;
&:hover {
background-color: unset;
}
}
}

31
test/specs/dom_components/model/Component.js

@ -3,6 +3,7 @@ import DomComponents from 'dom_components';
import Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage';
import ComponentText from 'dom_components/model/ComponentText';
import ComponentTextNode from 'dom_components/model/ComponentTextNode';
import ComponentLink from 'dom_components/model/ComponentLink';
import ComponentMap from 'dom_components/model/ComponentMap';
import ComponentVideo from 'dom_components/model/ComponentVideo';
@ -481,6 +482,36 @@ describe('Text Component', () => {
});
});
describe('Text Node Component', () => {
beforeEach(() => {
obj = new ComponentTextNode();
});
afterEach(() => {
obj = null;
});
test('Has content property', () => {
expect(obj.has('content')).toEqual(true);
});
test('Not droppable', () => {
expect(obj.get('droppable')).toEqual(false);
});
test('Not editable', () => {
expect(obj.get('editable')).toEqual(true);
});
test('Component toHTML with attributes', () => {
obj = new ComponentTextNode({
attributes: { 'data-test': 'value' },
content: `test content &<>"'`
});
expect(obj.toHTML()).toEqual('test content &amp;&lt;&gt;&quot;&#039;');
});
});
describe('Link Component', () => {
const aEl = document.createElement('a');

Loading…
Cancel
Save