├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config ├── install │ └── patternkit.settings.yml ├── optional │ ├── tour.tour.patternkit_admin.yml │ └── tour.tour.patternkit_block.yml └── schema │ ├── patternkit.data_types.schema.yml │ └── patternkit.schema.yml ├── css ├── bootstrap.min.css ├── cygnet │ ├── cygnet--overrides.css │ ├── cygnet--prefixed-for-drupal--upstream.css │ ├── cygnet--prefixed-for-drupal.css │ └── cygnet.css ├── patternkit.css ├── prosemirror.css ├── quill.snow.css └── sceditor │ ├── default.min.css │ └── famfamfam.png ├── drupalci.yml ├── drush.services.yml ├── fonts └── materialicons.woff2 ├── gulpfile.js ├── js ├── README.md ├── ajv.min.js ├── ajv.min.js.map ├── handlebars.js ├── handlebars.min.js ├── jsoneditor.js ├── nonmin │ ├── jsoneditor.js │ └── jsoneditor.js.map ├── patternkit.jsoneditor.ckeditor.es6.js ├── patternkit.jsoneditor.cygnet.es6.js ├── patternkit.jsoneditor.editor.array.es6.js ├── patternkit.jsoneditor.editor.object.es6.js ├── patternkit.jsoneditor.es6.js ├── patternkit.jsoneditor.js ├── patternkit.jsoneditor.prosemirror.es6.js ├── patternkit.jsoneditor.quill.es6.js ├── quill.js ├── quill.min.js └── sceditor.min.js ├── modules ├── patternkit_example │ ├── README.md │ ├── composer.json │ ├── lib │ │ └── patternkit │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .nvmrc │ │ │ ├── .patternlabrc │ │ │ ├── .sass-lint.yml │ │ │ ├── Gulpfile.js │ │ │ ├── README.md │ │ │ ├── dist │ │ │ ├── patternkit.css │ │ │ └── patternkit.min.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── src │ │ │ └── atoms │ │ │ │ ├── example │ │ │ │ ├── dist │ │ │ │ │ ├── example.css │ │ │ │ │ └── example.min.js │ │ │ │ ├── index.js │ │ │ │ └── src │ │ │ │ │ ├── example.json │ │ │ │ │ ├── example.md │ │ │ │ │ ├── example.scss │ │ │ │ │ ├── example.ts │ │ │ │ │ ├── example.twig │ │ │ │ │ └── example.yml │ │ │ │ ├── example_filtered │ │ │ │ ├── dist │ │ │ │ │ ├── example_filtered.css │ │ │ │ │ └── example_filtered.min.js │ │ │ │ ├── index.js │ │ │ │ └── src │ │ │ │ │ ├── example_filtered.json │ │ │ │ │ ├── example_filtered.md │ │ │ │ │ ├── example_filtered.scss │ │ │ │ │ ├── example_filtered.ts │ │ │ │ │ ├── example_filtered.twig │ │ │ │ │ └── example_filtered.yml │ │ │ │ └── example_ref │ │ │ │ ├── dist │ │ │ │ ├── example_ref.css │ │ │ │ └── example_ref.min.js │ │ │ │ ├── index.js │ │ │ │ └── src │ │ │ │ ├── example_ref.json │ │ │ │ ├── example_ref.md │ │ │ │ ├── example_ref.scss │ │ │ │ ├── example_ref.ts │ │ │ │ ├── example_ref.twig │ │ │ │ └── example_ref.yml │ │ │ └── tslint.json │ ├── patternkit_example.info.yml │ ├── patternkit_example.libraries.yml │ └── patternkit_example.module └── patternkit_media_library │ ├── .babelrc │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── composer.json │ ├── config │ ├── install │ │ └── patternkit_media_library.settings.yml │ └── schema │ │ └── patternkit_media_library.schema.yml │ ├── css │ └── media-library-modal.css │ ├── gulpfile.js │ ├── js │ ├── patternkit.jsoneditor.media_library.es6.js │ └── patternkit.jsoneditor.media_library.js │ ├── npm-shrinkwrap.json │ ├── package.json │ ├── patternkit_media_library.info.yml │ ├── patternkit_media_library.install │ ├── patternkit_media_library.libraries.yml │ ├── patternkit_media_library.links.task.yml │ ├── patternkit_media_library.module │ ├── patternkit_media_library.routing.yml │ ├── patternkit_media_library.services.yml │ └── src │ ├── Controller │ └── PatternkitMediaLibraryController.php │ ├── Form │ └── MediaLibrarySettingsForm.php │ └── MediaLibraryJSONLibraryOpener.php ├── npm-shrinkwrap.json ├── package.json ├── patternkit.api.php ├── patternkit.info.yml ├── patternkit.install ├── patternkit.libraries.yml ├── patternkit.links.action.yml ├── patternkit.links.contextual.yml ├── patternkit.links.menu.yml ├── patternkit.links.task.yml ├── patternkit.module ├── patternkit.polyfil.php ├── patternkit.routing.yml ├── patternkit.services.yml ├── src ├── AJAX │ └── PatternkitEditorUpdateCommand.php ├── Annotation │ ├── PatternFieldProcessor.php │ └── PatternLibrary.php ├── Asset │ ├── Library.php │ ├── LibraryInterface.php │ ├── LibraryNamespaceResolver.php │ ├── LibraryNamespaceResolverInterface.php │ ├── PatternDiscovery.php │ ├── PatternDiscoveryCollector.php │ ├── PatternDiscoveryInterface.php │ ├── PatternLibraryParser │ │ ├── FilePatternLibraryParser.php │ │ ├── JSONPatternLibraryParser.php │ │ ├── RESTPatternLibraryParser.php │ │ ├── TwigPatternLibraryParser.php │ │ └── WebcomponentPatternLibraryParser.php │ ├── PatternLibraryParserBase.php │ └── PatternLibraryParserInterface.php ├── Commands │ └── PatternkitCommands.php ├── Controller │ └── PatternkitController.php ├── Element │ ├── Pattern.php │ └── PatternError.php ├── Entity │ ├── Pattern.php │ ├── PatternInterface.php │ ├── PatternRouteProvider.php │ └── PatternkitBlock.php ├── Exception │ ├── SchemaException.php │ ├── SchemaReferenceException.php │ └── SchemaValidationException.php ├── Form │ ├── PatternLibraryJSONForm.php │ ├── PatternkitForm.php │ ├── PatternkitSettingsForm.php │ └── PatternkitTranslateBlockForm.php ├── FormElement │ └── PatternkitJson.php ├── JSONSchemaEditorTrait.php ├── Loader │ └── PatternLibraryLoader.php ├── PathProcessor │ └── PathProcessorPatterns.php ├── PatternEditorConfig.php ├── PatternFieldProcessorPluginManager.php ├── PatternLibrary.php ├── PatternLibraryJSONParserTrait.php ├── PatternLibraryPluginDefault.php ├── PatternLibraryPluginDefinition.php ├── PatternLibraryPluginInterface.php ├── PatternLibraryPluginManager.php ├── PatternTranslationHandler.php ├── Plugin │ ├── Block │ │ └── PatternkitBlock.php │ ├── Derivative │ │ └── PatternkitBlock.php │ ├── Field │ │ └── FieldType │ │ │ └── SerializedData.php │ ├── Menu │ │ └── LocalAction │ │ │ └── PatternkitAddLocalAction.php │ ├── PatternFieldProcessor │ │ ├── PatternFieldProcessorBase.php │ │ ├── PatternFieldProcessorInterface.php │ │ ├── TokenProcessor.php │ │ └── WysiwygFieldProcessor.php │ └── PatternLibrary │ │ ├── PatternLibraryFile.php │ │ ├── PatternLibraryJSON.php │ │ ├── PatternLibraryREST.php │ │ └── PatternLibraryTwig.php ├── Routing │ └── PatternkitTranslateBlockFormRouteSubscriber.php ├── Schema │ ├── DataPreProcessor │ │ └── ObjectCoercionDataPreProcessor.php │ ├── PatternkitRefProvider.php │ ├── SchemaFactory.php │ ├── SchemaHelper.php │ ├── SchemaIterator.php │ ├── SchemaWalker.php │ └── SchemaWalkerFactory.php └── StreamWrapper │ ├── LibraryStream.php │ └── PatternkitStream.php ├── templates └── patternkit-add-list.html.twig └── tests ├── modules └── patternkit_test │ ├── lib │ ├── patternkit_test.info.yml │ ├── patternkit_test.libraries.yml │ └── src │ └── Plugin │ └── PatternFieldProcessor │ └── ExceptionThrowerProcessor.php └── src ├── Functional ├── PatternkitSettingsTest.php └── PatternkitTest.php ├── Kernel ├── Asset │ ├── LibraryNamespaceResolverTest.php │ ├── LibraryTest.php │ ├── PatternDiscoveryTest.php │ └── PatternLibraryParser │ │ ├── JsonPatternLibraryParserTest.php │ │ ├── PatternLibraryParserTestBase.php │ │ └── TwigPatternLibraryParserTest.php ├── Loader │ └── PatternLibraryLoaderTest.php └── PatternFieldProcessorPluginManagerTest.php ├── Traits ├── JsonDecodeTrait.php ├── PatternkitBlockHelperTrait.php ├── SchemaFixtureTrait.php └── SchemaHelperTestTrait.php └── Unit ├── Asset ├── LibraryTest.php └── TestLibrary.php ├── PatternElementTest.php ├── PatternFieldProcessorPluginManagerTest.php ├── Plugin └── PatternFieldProcessor │ ├── TokenProcessorTest.php │ └── WysiwygFieldProcessorTest.php └── Schema ├── PatternkitRefProviderTest.php ├── SchemaFactoryTest.php ├── SchemaHelperTest.php ├── SchemaIteratorTest.php ├── SchemaWalkerTest.php └── TestPatternkitRefProvider.php /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "ie >= 11"] 8 | }, 9 | "forceAllTransforms": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cybtachyon] 4 | patreon: cybtachyon 5 | ko_fi: cybtachyon 6 | tidelift: packagist/drupal/patternkit 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | js/globals**.js** 3 | node_modules 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cyb.tachyon@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/patternkit", 3 | "description": "Patternkit is a Drupal module that lets you drag and drop your theme templates, patterns, and components into layouts, whether using Layout Builder, Panels, or the default Block Layout editor.", 4 | "type": "drupal-module", 5 | "authors": [ 6 | { 7 | "name": "Derek Reese", 8 | "email": "cyb.tachyon@gmail.com" 9 | }, 10 | { 11 | "name": "Red Hat Inc.", 12 | "email": "drupal@redhat.com" 13 | } 14 | ], 15 | "license": "MIT", 16 | "require": { 17 | "php": ">=7.4.0", 18 | "ext-json": "*", 19 | "swaggest/json-schema": "^0.12", 20 | "symfony/finder": "^3.4 || ^4.0", 21 | "symfony/yaml": "^3.4 || ^4.0", 22 | "webmozart/path-util": "^2.1.0" 23 | }, 24 | "require-dev": { 25 | "drupal/core-recommended": "^9", 26 | "drupal/core-dev": "^9", 27 | "drupal/devel": "^4.1" 28 | }, 29 | "extra": { 30 | "drush": { 31 | "services": { 32 | "drush.services.yml": "^9" 33 | } 34 | } 35 | }, 36 | "repositories": [], 37 | "scripts": { 38 | "post-autoload-dump": [ 39 | "wget -Oq -P js/ --no-check-certificate https://raw.github.com/jdorn/json-editor/master/dist/jsoneditor.min.js", 40 | "wget -Oq -P js/ --no-check-certificate https://raw.github.com/jdorn/json-editor/master/dist/jsoneditor.js" 41 | ], 42 | "js:build": [ 43 | ". ~/.profile && nvm install && nvm use", 44 | "npm install && npx gulp" 45 | ] 46 | }, 47 | "suggest": { 48 | "drupal/entity_browser": "Improves display for the patternkit_media_library module.", 49 | "drupal/media_entity_browser": "Improves display for the patternkit_media_library module." 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/install/patternkit.settings.yml: -------------------------------------------------------------------------------- 1 | patternkit_cache_enabled: true 2 | patternkit_json_editor_css: "" 3 | patternkit_json_editor_icons: "" 4 | patternkit_json_editor_js: "" 5 | patternkit_json_editor_theme: cygnet 6 | patternkit_libraries: [] 7 | patternkit_json_editor_use_shadow_dom: true 8 | patternkit_json_editor_wysiwyg: "" 9 | patternkit_json_editor_ckeditor_toolbar: "" 10 | patternkit_json_editor_disable_properties_buttons: false 11 | patternkit_json_editor_disable_edit_json_button: true 12 | -------------------------------------------------------------------------------- /config/optional/tour.tour.patternkit_admin.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - patternkit 6 | id: patternkit_admin 7 | label: 'Patternkit admin settings page' 8 | module: patternkit 9 | routes: 10 | - 11 | route_name: patternkit.settings 12 | tips: 13 | patternkit-libraries: 14 | id: patternkit-libraries 15 | plugin: text 16 | label: 'Manage Patternkit library usage and visibility' 17 | body: 'Disabled libraries will not be usable for twig or blocks. Invisible libraries will not appear in lists.' 18 | weight: 1 19 | attributes: 20 | data-id: edit-patternkit-libraries 21 | patternkit-library-cache: 22 | id: patternkit-library-cache 23 | plugin: text 24 | label: 'Set library cache usage' 25 | body: 'Controlled primarily by the Drupal discovery cache, this cache can be cleared with drush cr --cache-clear=discovery.' 26 | weight: 2 27 | attributes: 28 | data-id: edit-patternkit-cache-enabled 29 | patternkit-disk-cache: 30 | id: patternkit-disk-cache 31 | plugin: text 32 | label: 'Set disk cache usage' 33 | body: 'Useful for REST libraries, the disk cache controls if a request is made for new patterns on each page load.' 34 | weight: 3 35 | attributes: 36 | data-id: edit-patternkit-render-cache 37 | patternkit-ttl: 38 | id: patternkit-ttl 39 | plugin: text 40 | label: 'Set Time-To-Live settings' 41 | body: 'Limit the amount of time the Patternkit Disk cache will save REST API requested patterns before requesting a new one from the REST server.' 42 | weight: 4 43 | attributes: 44 | data-id: edit-patternkit-default-module-ttl 45 | -------------------------------------------------------------------------------- /config/optional/tour.tour.patternkit_block.yml: -------------------------------------------------------------------------------- 1 | langcode: en 2 | status: true 3 | dependencies: 4 | module: 5 | - patternkit 6 | id: patternkit_block 7 | label: 'Patternkit Layout Builder Add Block page' 8 | module: patternkit 9 | routes: 10 | - 11 | route_name: layout_builder.add_block 12 | tips: 13 | patternkit-context-node: 14 | id: patternkit-context-node 15 | plugin: text 16 | label: 'Add route context from the node' 17 | body: 'Set this to allow the block to use tokens from the node context.' 18 | weight: 1 19 | attributes: 20 | data-class: form-item-settings-context-mapping-node 21 | patternkit-context-user: 22 | id: patternkit-context-user 23 | plugin: text 24 | label: 'Add route context from the user' 25 | body: 'Set this to allow the block to use tokens from the user context.' 26 | weight: 2 27 | attributes: 28 | data-class: form-item-settings-context-mapping-user 29 | patternkit-reusable: 30 | id: patternkit-reusable 31 | plugin: text 32 | label: 'Make the block reusable' 33 | body: 'This allows you to make the block reusable, where it will show up in block listings and can be used on other layouts and block placements on the site.' 34 | weight: 3 35 | attributes: 36 | data-class: form-item-settings-reusable 37 | patternkit-presentation: 38 | id: patternkit-presentation 39 | plugin: text 40 | label: 'Presentation settings' 41 | body: 'Additional Patternkit plugins can add support for other presentation styles, such as web components, JSON, or JavaScript modules.' 42 | weight: 4 43 | attributes: 44 | data-class: form-item-settings-presentation-style 45 | patternkit-tokens: 46 | id: patternkit-tokens 47 | plugin: text 48 | label: 'Token replacements' 49 | body: 'Use these tokens provided by context to insert those values into the pattern.' 50 | weight: 5 51 | attributes: 52 | data-id: edit-settings-global-tokens 53 | -------------------------------------------------------------------------------- /config/schema/patternkit.data_types.schema.yml: -------------------------------------------------------------------------------- 1 | patternkit.json: 2 | type: string 3 | label: 'Text with text format' 4 | # We declare the entire mapping of text and text format as translatable. This 5 | # causes the entire mapping to be saved to the language overrides of the 6 | # configuration. Storing only the (to be formatted) text could result in 7 | # security problems in case the text format of the source text is changed. 8 | translatable: true 9 | form_element_class: 'Drupal\patternkit\FormElement\PatternkitJson' 10 | -------------------------------------------------------------------------------- /config/schema/patternkit.schema.yml: -------------------------------------------------------------------------------- 1 | patternkit.settings: 2 | type: config_object 3 | label: 'Patternkit Module Settings' 4 | mapping: 5 | patternkit_cache_enabled: 6 | type: boolean 7 | label: 'Boolean that sets if the Patternkit cache is used or not.' 8 | patternkit_json_editor_css: 9 | type: string 10 | label: 'Comma-separated string of stylesheet files to load with the JSON Editor.' 11 | patternkit_json_editor_icons: 12 | type: string 13 | label: 'String selecting the icon set to be used for the JSON Editor.' 14 | patternkit_json_editor_js: 15 | type: string 16 | label: 'Comma-separated string of Javascript files to load with the JSON Editor.' 17 | patternkit_json_editor_theme: 18 | type: string 19 | label: 'String selecting the theme to be used for the JSON Editor.' 20 | patternkit_libraries: 21 | type: mapping 22 | label: 'A list of settings for each Patternkit library.' 23 | mapping: 24 | enabled: 25 | type: boolean 26 | label: 'Library Enabled' 27 | visible: 28 | type: boolean 29 | label: 'Library visible in lists' 30 | patternkit_json_editor_use_shadow_dom: 31 | type: boolean 32 | label: 'Boolean that determines whether to use the Shadow DOM for the JSON Editor.' 33 | patternkit_json_editor_wysiwyg: 34 | type: string 35 | label: 'String selecting the WYSIWYG tool to use in the JSON Editor.' 36 | patternkit_json_editor_ckeditor_toolbar: 37 | type: string 38 | label: 'String selecting the CKEditor text format to use for CKEditor inside the JSON Editor.' 39 | patternkit_json_editor_disable_properties_buttons: 40 | type: boolean 41 | label: 'Boolean that determines whether to show the properties buttons.' 42 | patternkit_json_editor_disable_edit_json_button: 43 | type: boolean 44 | label: 'Boolean that determines whether to show the JSON Edit button.' 45 | 46 | block.settings.patternkit_block:*: 47 | type: block_settings 48 | mapping: 49 | pattern: 50 | type: integer 51 | label: 'Revision ID of the pattern in use on the block.' 52 | patternkit_block_id: 53 | type: integer 54 | label: 'ID of the Patternkit Block content entity.' 55 | patternkit_block_rid: 56 | type: integer 57 | label: 'Revision ID of the Patternkit Block content entity.' 58 | presentation_style: 59 | type: string 60 | label: 'Presentation style for the patternkit block when rendered.' 61 | version: 62 | type: string 63 | label: 'Version of the pattern or containing library.' 64 | form_element_class: 'Drupal\patternkit\FormElement\PatternkitJson' 65 | -------------------------------------------------------------------------------- /css/cygnet/cygnet--overrides.css: -------------------------------------------------------------------------------- 1 | #drupal-off-canvas [data-theme=cygnet] label { 2 | color: inherit; 3 | font-size: inherit; 4 | font-weight: inherit; 5 | line-height: inherit; 6 | } 7 | 8 | #drupal-off-canvas [data-theme=cygnet] label { 9 | font-family: 'RedHatText', Helvetica, Arial, sans-serif; 10 | } 11 | #drupal-off-canvas [data-theme=cygnet] button { 12 | font-family: 'RedHatText', Helvetica, Arial, sans-serif; 13 | } 14 | 15 | /* 16 | * Upstream styles in node_modules/@json-editor/json-editor/src/style.css set 17 | * the height to 300px, which is too tall for all textareas. The browser 18 | * provides a tooltip to resize the element. 19 | */ 20 | #drupal-off-canvas .je-textarea { 21 | height: auto; 22 | } 23 | -------------------------------------------------------------------------------- /css/cygnet/cygnet--prefixed-for-drupal--upstream.css: -------------------------------------------------------------------------------- 1 | #drupal-off-canvas .je-float-right-linkholder { 2 | float: right; 3 | margin-left: 10px; 4 | } 5 | 6 | #drupal-off-canvas .je-modal { 7 | background-color: white; 8 | border: 1px solid black; 9 | box-shadow: 3px 3px black; 10 | position: absolute; 11 | z-index: 10; 12 | } 13 | 14 | #drupal-off-canvas .je-infobutton-icon { 15 | font-size: 16px; 16 | font-weight: bold; 17 | padding: 0.25rem; 18 | position: relative; 19 | display: inline-block; 20 | } 21 | 22 | #drupal-off-canvas .je-infobutton-tooltip { 23 | font-size: 12px; 24 | font-weight: normal; 25 | font-family: sans-serif; 26 | visibility: hidden; 27 | background-color: rgba(50, 50, 50, 0.75); 28 | margin: 0 0.25rem; 29 | color: #fafafa; 30 | padding: 0.5rem 1rem; 31 | border-radius: 0.25rem; 32 | width: 20rem; 33 | position: absolute; 34 | } 35 | 36 | #drupal-off-canvas .je-not-loaded { 37 | pointer-events: none; 38 | } 39 | #drupal-off-canvas .je-header { 40 | display: inline-block 41 | } 42 | 43 | #drupal-off-canvas .je-upload-preview img { 44 | float: left; 45 | margin: 0 0.5rem 0.5rem 0; 46 | max-width: 100%; 47 | max-height: 5rem; 48 | } 49 | 50 | #drupal-off-canvas .je-checkbox { 51 | display: inline-block; 52 | width: auto 53 | } 54 | 55 | #drupal-off-canvas .je-checkbox-control--compact { 56 | display: inline-block; 57 | margin-right: 1rem 58 | } 59 | 60 | #drupal-off-canvas .je-radio { 61 | display: inline-block; 62 | width: auto 63 | } 64 | 65 | #drupal-off-canvas .je-radio-control--compact { 66 | display: inline-block; 67 | margin-right: 1rem 68 | } 69 | 70 | #drupal-off-canvas .je-switcher { 71 | background-color: transparent; 72 | display: inline-block; 73 | font-style: italic; 74 | font-weight: normal; 75 | height: auto; 76 | width: auto; 77 | margin-bottom: 0; 78 | margin-left: 5px; 79 | padding: 0 0 0 3px; 80 | } 81 | 82 | #drupal-off-canvas .je-textarea { 83 | width: 100%; 84 | height: 300px; 85 | box-sizing: border-box 86 | } 87 | 88 | #drupal-off-canvas .je-range-control { 89 | text-align: center 90 | } 91 | 92 | #drupal-off-canvas .je-indented-panel { 93 | padding-left: 10px; 94 | margin-left: 10px; 95 | border-left: 1px solid #ccc 96 | } 97 | 98 | #drupal-off-canvas .je-indented-panel--top { 99 | padding-left: 10px; 100 | margin-left: 10px; 101 | } 102 | 103 | #drupal-off-canvas .je-tabholder { 104 | float: left; 105 | width: 130px; 106 | } 107 | 108 | #drupal-off-canvas .je-tabholder .content { 109 | margin-left: 120px; 110 | } 111 | 112 | #drupal-off-canvas .je-tabholder--top { 113 | margin-left: 10px; 114 | } 115 | 116 | #drupal-off-canvas .je-tabholder--clear { 117 | clear:both; 118 | } 119 | 120 | #drupal-off-canvas .je-tab { 121 | border: 1px solid #ccc; 122 | border-width: 1px 0 1px 1px; 123 | text-align: center; 124 | line-height: 30px; 125 | border-radius: 5px; 126 | border-bottom-right-radius: 0; 127 | border-top-right-radius: 0; 128 | font-weight: bold; 129 | cursor: pointer 130 | } 131 | 132 | #drupal-off-canvas .je-tab--top { 133 | float: left; 134 | border: 1px solid #ccc; 135 | border-width: 1px 1px 0px 1px; 136 | text-align: center; 137 | line-height: 30px; 138 | border-radius: 5px; 139 | padding-left: 5px; 140 | padding-right: 5px; 141 | border-bottom-right-radius: 0; 142 | border-bottom-left-radius: 0; 143 | font-weight: bold; 144 | cursor: pointer 145 | } 146 | 147 | #drupal-off-canvas .je-block-link { 148 | display: block 149 | } 150 | 151 | #drupal-off-canvas .je-media { 152 | width: 100% 153 | } -------------------------------------------------------------------------------- /css/patternkit.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Miscellaneous styles for Patternkit interfaces. 3 | */ 4 | 5 | /* 6 | * Implements spinning loader to display while 7 | * JSON Editor's schemas are being loaded. 8 | */ 9 | .js-patternkit-loading__on { 10 | border: 4px solid #ebebeb; /* Light grey */ 11 | border-top: 4px solid #0066cc; /* Blue */ 12 | border-radius: 50%; 13 | width: 30px; 14 | height: 30px; 15 | margin: 40px auto; 16 | animation: spin 1s linear infinite; 17 | } 18 | 19 | @keyframes spin { 20 | 0% { transform: rotate(0deg); } 21 | 100% { transform: rotate(360deg); } 22 | } 23 | -------------------------------------------------------------------------------- /css/prosemirror.css: -------------------------------------------------------------------------------- 1 | .ProseMirror { 2 | position: relative; 3 | } 4 | 5 | .ProseMirror { 6 | word-wrap: break-word; 7 | white-space: pre-wrap; 8 | white-space: break-spaces; 9 | -webkit-font-variant-ligatures: none; 10 | font-variant-ligatures: none; 11 | font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ 12 | } 13 | 14 | .ProseMirror pre { 15 | white-space: pre-wrap; 16 | } 17 | 18 | .ProseMirror li { 19 | position: relative; 20 | } 21 | 22 | .ProseMirror-hideselection *::selection { background: transparent; } 23 | .ProseMirror-hideselection *::-moz-selection { background: transparent; } 24 | .ProseMirror-hideselection { caret-color: transparent; } 25 | 26 | .ProseMirror-selectednode { 27 | outline: 2px solid #8cf; 28 | } 29 | 30 | /* Make sure li selections wrap around markers */ 31 | 32 | li.ProseMirror-selectednode { 33 | outline: none; 34 | } 35 | 36 | li.ProseMirror-selectednode:after { 37 | content: ""; 38 | position: absolute; 39 | left: -32px; 40 | right: -2px; top: -2px; bottom: -2px; 41 | border: 2px solid #8cf; 42 | pointer-events: none; 43 | } 44 | -------------------------------------------------------------------------------- /css/sceditor/famfamfam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/css/sceditor/famfamfam.png -------------------------------------------------------------------------------- /drupalci.yml: -------------------------------------------------------------------------------- 1 | build: 2 | assessment: 3 | validate_codebase: 4 | phplint: 5 | csslint: 6 | eslint: 7 | phpcs: 8 | testing: 9 | run_tests.standard: 10 | types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' 11 | suppress-deprecations: false 12 | run_tests.js: 13 | concurrency: 1 14 | types: 'PHPUnit-FunctionalJavascript' 15 | suppress-deprecations: false 16 | -------------------------------------------------------------------------------- /drush.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | patternkit.commands: 3 | class: Drupal\patternkit\Commands\PatternkitCommands 4 | tags: 5 | - { name: drush.command } 6 | -------------------------------------------------------------------------------- /fonts/materialicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/fonts/materialicons.woff2 -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babelify = require('babelify'); 3 | const browserify = require("browserify"); 4 | const buffer = require('vinyl-buffer'); 5 | const source = require('vinyl-source-stream'); 6 | const prefixCss = require('gulp-prefix-css'); 7 | const rename = require('gulp-rename'); 8 | 9 | const gulpConfig = { 10 | src: './js/**es6.js', 11 | dist: './js', 12 | main: 'patternkit.jsoneditor.es6.js', 13 | lib: [ 14 | './node_modules/@json-editor/json-editor/dist/**', 15 | './node_modules/ajv/dist/ajv.min.js', 16 | './node_modules/ajv/dist/ajv.min.js.map', 17 | './node_modules/handlebars/dist/handlebars.js', 18 | './node_modules/handlebars/dist/handlebars.min.js', 19 | ] 20 | }; 21 | 22 | // @todo Move to Rollup from Browserify to support modular bundling. 23 | gulp.task('compile:es6', function () { 24 | return browserify(gulpConfig.dist + '/' + gulpConfig.main) 25 | .transform(babelify) 26 | .bundle() 27 | .pipe(source(gulpConfig.main.replace('.es6', ''))) 28 | .pipe(buffer()) 29 | .pipe(gulp.dest(gulpConfig.dist)); 30 | }); 31 | 32 | gulp.task('copy:lib', function() { 33 | return gulp.src(gulpConfig.lib).pipe(gulp.dest(gulpConfig.dist)); 34 | }); 35 | 36 | // Prefixes upstream JSON Editor's core styles so they can be used in non-shadow 37 | // DOM version of Cygnet. 38 | const drupalModalId = '#drupal-off-canvas'; 39 | gulp.task('prefix-css:cygnet-theme-upstream', function(){ 40 | return gulp.src('./node_modules/@json-editor/json-editor/src/style.css') 41 | .pipe(prefixCss(drupalModalId)) 42 | .pipe(rename('cygnet--prefixed-for-drupal--upstream.css')) 43 | .pipe(gulp.dest('./css/cygnet')); 44 | }); 45 | 46 | // Prefixes Cygnet's styles so they work when JSON Editor gets loaded outside 47 | // the shadow DOM. 48 | gulp.task('prefix-css:cygnet-theme', function(){ 49 | return gulp.src('./css/cygnet/cygnet.css') 50 | .pipe(prefixCss(drupalModalId)) 51 | .pipe(rename('cygnet--prefixed-for-drupal.css')) 52 | .pipe(gulp.dest('./css/cygnet')); 53 | }); 54 | 55 | gulp.task('watch', function() { 56 | return gulp.watch(gulpConfig.src, 'compile:es6'); 57 | }); 58 | 59 | gulp.task('default', gulp.parallel(['compile:es6', 'copy:lib', 'prefix-css:cygnet-theme', 'prefix-css:cygnet-theme-upstream'])); 60 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Patternkit JavaScript Dependencies 2 | 3 | ## JSON Editor JS 4 | 5 | - patternkit.jsoneditor.js is built with the Gulp script from patternkit.jsoneditor.es6.js. 6 | 7 | ## Dependencies 8 | 9 | - AJV and Json Schema Editor are built using package.json's Gulp script. 10 | 11 | ## Getting Started 12 | 13 | - Use Node Version Manager 14 | 15 | 16 | nvm use 17 | 18 | - Run the build script to update the dependencies and transpile the JSON Schema Editor Patternkit JS. 19 | 20 | 21 | npm run build 22 | -------------------------------------------------------------------------------- /js/patternkit.jsoneditor.cygnet.es6.js: -------------------------------------------------------------------------------- 1 | class cygnetTheme extends JSONEditor.AbstractTheme { 2 | getFormInputLabel (text, req) { 3 | const el = super.getFormInputLabel(text, req) 4 | el.classList.add('je-cygnet-form-input-label') 5 | return el 6 | } 7 | 8 | getFormInputDescription (text) { 9 | const el = super.getFormInputDescription(text) 10 | el.classList.add('je-cygnet-form-input-label') 11 | el.innerHTML = text 12 | return el 13 | } 14 | 15 | getIndentedPanel () { 16 | const el = super.getIndentedPanel() 17 | el.classList.add('je-cygnet-indented-panel') 18 | el.style = el.style || {}; 19 | return el 20 | } 21 | 22 | getTopIndentedPanel () { 23 | return this.getIndentedPanel() 24 | } 25 | 26 | getChildEditorHolder () { 27 | const el = super.getChildEditorHolder() 28 | el.classList.add('je-cygnet-child-editor-holder') 29 | return el 30 | } 31 | 32 | getDescription (text) { 33 | const el = super.getDescription(text) 34 | el.classList.add('je-cygnet-description') 35 | el.innerHTML = text 36 | return el 37 | } 38 | 39 | // If no title, use the text as title so that we have can use the 40 | // title attr as a CSS selector to style the collapse/expand state. 41 | setButtonText (button, text, icon, title) { 42 | if (!title && text) { 43 | title = text; 44 | } 45 | 46 | if (text == "Object Properties") { 47 | text = "Properties"; 48 | } 49 | 50 | return super.setButtonText(button, text, icon, title); 51 | } 52 | 53 | getHeaderButtonHolder () { 54 | const el = this.getButtonHolder() 55 | el.classList.add('je-cygnet-header-button-holder') 56 | el.style.display = 'block'; 57 | return el 58 | } 59 | 60 | getTable () { 61 | const el = super.getTable() 62 | el.classList.add('je-table') 63 | return el 64 | } 65 | 66 | setGridColumnSize (el, size) { 67 | el.className = 'col-md-'+size; 68 | } 69 | 70 | addInputError (input, text) { 71 | const group = this.closest(input, '.form-control') || input.controlgroup 72 | 73 | if (!input.errmsg) { 74 | input.errmsg = document.createElement('div') 75 | input.errmsg.setAttribute('class', 'errmsg') 76 | input.errmsg.style = input.errmsg.style || {} 77 | input.errmsg.style.color = 'red' 78 | group.appendChild(input.errmsg) 79 | } else { 80 | input.errmsg.style.display = 'block' 81 | } 82 | 83 | input.errmsg.innerHTML = '' 84 | input.errmsg.appendChild(document.createTextNode(text)) 85 | } 86 | 87 | removeInputError (input) { 88 | if (input.style) { 89 | input.style.borderColor = '' 90 | } 91 | if (input.errmsg) input.errmsg.style.display = 'none' 92 | } 93 | 94 | getTabHolder (propertyName) { 95 | var pName = typeof propertyName === 'undefined' ? '' : propertyName; 96 | var el = document.createElement('div'); 97 | el.classList.add('je-cygnet-tabs'); 98 | el.innerHTML = "
"); 99 | return el; 100 | } 101 | 102 | getTab (span, tabId) { 103 | const el = document.createElement('div') 104 | el.appendChild(span) 105 | el.id = tabId 106 | el.style = el.style || {} 107 | el.classList.add('je-cygnet-tab'); 108 | return el 109 | } 110 | 111 | markTabActive (row) { 112 | row.tab.classList.remove('je-cygnet-tab--inactive'); 113 | row.tab.classList.add('je-cygnet-tab--active'); 114 | 115 | if (typeof row.rowPane !== 'undefined') { 116 | row.rowPane.style.display = '' 117 | } else { 118 | row.container.style.display = '' 119 | } 120 | } 121 | 122 | markTabInactive (row) { 123 | row.tab.classList.remove('je-cygnet-tab--active'); 124 | row.tab.classList.add('je-cygnet-tab--inactive'); 125 | 126 | if (typeof row.rowPane !== 'undefined') { 127 | row.rowPane.style.display = 'none' 128 | } else { 129 | row.container.style.display = 'none' 130 | } 131 | } 132 | 133 | } 134 | 135 | export function patternkitEditorCygnet($, Drupal, JSONEditor) { 136 | 'use strict'; 137 | Drupal.behaviors.patternkitEditorCygnet = { 138 | attach: function (context, settings) { 139 | if (!window.JSONEditor) { 140 | return; 141 | } 142 | cygnetTheme.rules = { } 143 | JSONEditor.defaults.themes.cygnet = cygnetTheme; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /js/patternkit.jsoneditor.editor.array.es6.js: -------------------------------------------------------------------------------- 1 | /*globals Console:false */ 2 | /*globals Drupal:false */ 3 | /*globals jQuery:false */ 4 | /*globals JSONEditor:false */ 5 | 6 | /** 7 | * Duplicates json-editor trigger utility. 8 | * 9 | * Cannot figure out how to import it without errors. 10 | * The function is defined in @json-editor/json-editor/src/utilities. 11 | * 12 | * @param el 13 | * @param event 14 | */ 15 | const trigger = function (el, event) { 16 | const e = document.createEvent('HTMLEvents') 17 | e.initEvent(event, true, true) 18 | el.dispatchEvent(e) 19 | }; 20 | 21 | /** 22 | * @file PatternkitJsoneditorEditorArray class. 23 | * 24 | * @external Drupal 25 | * @external jQuery 26 | * @external JSONEditor 27 | */ 28 | class PatternkitJsoneditorEditorArray extends JSONEditor.defaults.editors.array { 29 | /** 30 | * Overrides json-editor array _createToggleButton() method. 31 | * 32 | * Overrides JSONEditor's _createToggleButton() method for arrays. The only change is to 33 | * trigger toggle of section if user clicks on the label/title, not just on 34 | * the expand/collapse button. Makes hiding/showing sections much easier. 35 | */ 36 | _createToggleButton () { 37 | const button = this.getButton('', 'collapse', 'button_collapse') 38 | button.classList.add('json-editor-btntype-toggle') 39 | this.title.insertBefore(button, this.title.childNodes[0]) 40 | 41 | const rowHolderDisplay = this.row_holder.style.display 42 | const controlsDisplay = this.controls.style.display 43 | 44 | // 45 | // Replaces the click handler on the button (element `this.collapse_control`), 46 | // so that the section is toggled if you click either on the button or its label 47 | // (i.e., if you clicked anywhere on the title). 48 | this.title.classList.add('patternkit-jsoneditor-clickable'); 49 | this.title.addEventListener('click', e => { 50 | e.preventDefault() 51 | e.stopPropagation() 52 | if (this.collapsed) { 53 | this.collapsed = false 54 | if (this.panel) this.panel.style.display = '' 55 | this.row_holder.style.display = rowHolderDisplay 56 | if (this.tabs_holder) this.tabs_holder.style.display = '' 57 | this.controls.style.display = controlsDisplay 58 | this.setButtonText(this.toggle_button, '', 'collapse', this.translate('button_collapse')) 59 | } else { 60 | this.collapsed = true 61 | this.row_holder.style.display = 'none' 62 | if (this.tabs_holder) this.tabs_holder.style.display = 'none' 63 | this.controls.style.display = 'none' 64 | if (this.panel) this.panel.style.display = 'none' 65 | this.setButtonText(this.toggle_button, '', 'expand', this.translate('button_expand')) 66 | } 67 | }); 68 | // 69 | 70 | return button 71 | } 72 | } 73 | 74 | export function patternkitEditorArray($, Drupal, JSONEditor) { 75 | 'use strict'; 76 | Drupal.behaviors.patternkitEditorArray = { 77 | attach: function (context, settings) { 78 | if (!window.JSONEditor) { 79 | return; 80 | } 81 | JSONEditor.defaults.editors.patternkit_editor_array = PatternkitJsoneditorEditorArray; 82 | JSONEditor.defaults.resolvers.unshift(function (schema) { 83 | if (schema.type === 'array') { 84 | return 'patternkit_editor_array'; 85 | } 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /js/patternkit.jsoneditor.prosemirror.es6.js: -------------------------------------------------------------------------------- 1 | /*globals Console:false */ 2 | /*globals Drupal:false */ 3 | /*globals jQuery:false */ 4 | /*globals JSONEditor:false */ 5 | /** 6 | * @file DrupalImageEditor class. 7 | * 8 | * @external Drupal 9 | * @external jQuery 10 | * @external JSONEditor 11 | */ 12 | 13 | import {EditorState} from "prosemirror-state" 14 | import {EditorView} from "prosemirror-view" 15 | import {Schema, DOMParser} from "prosemirror-model" 16 | import {schema} from "prosemirror-schema-basic" 17 | import {addListNodes} from "prosemirror-schema-list" 18 | import {exampleSetup} from "prosemirror-example-setup" 19 | 20 | class DrupalProseMirror extends JSONEditor.defaults.editors.string { 21 | 22 | build() { 23 | // Override the format when building the base string editor. 24 | this.options.format = 'textarea'; 25 | super.build(); 26 | this.input_type = this.schema.format; 27 | this.input.setAttribute('data-schemaformat', this.input_type); 28 | } 29 | 30 | afterInputReady() { 31 | // Editor options. 32 | // @todo Replace JSONEditor.defaults with this.defaults. 33 | this.options = jQuery.extend({}, JSONEditor.defaults.options.drupal_prosemirror || {}, this.options.drupal_prosemirror || {}); 34 | 35 | const editorSchema = new Schema({ 36 | nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"), 37 | marks: schema.spec.marks 38 | }) 39 | 40 | this.prosemirror_container = document.createElement('div'); 41 | this.prosemirror_container.style.width = '100%'; 42 | this.prosemirror_container.style.position = 'relative'; 43 | 44 | this.input.style.display = 'none'; 45 | 46 | this.prosemirror_instance = new EditorView(this.prosemirror_container, { 47 | state: EditorState.create({ 48 | doc: DOMParser.fromSchema(editorSchema).parse(this.getValue()), 49 | plugins: exampleSetup({schema: editorSchema}) 50 | }) 51 | }); 52 | 53 | this.input.parentNode.insertBefore(this.prosemirror_container, this.input); 54 | 55 | if (this.schema.readOnly || this.schema.readonly || this.schema.template) { 56 | // Set all EditorProps editable to false; 57 | } 58 | 59 | // Handle editor change. 60 | 61 | this.theme.afterInputReady(this.input); 62 | } 63 | 64 | destroy() { 65 | if(this.prosemirror_instance) { 66 | this.prosemirror_instance.destroy(); 67 | this.prosemirror_instance = null; 68 | } 69 | super.destroy(); 70 | } 71 | 72 | disable(always_disabled) { 73 | if (always_disabled) { 74 | this.always_disabled = true; 75 | } 76 | if (this.prosemirror_instance) { 77 | this.prosemirror_instance.setProps({editable: false}); 78 | } 79 | super.disable(always_disabled); 80 | } 81 | 82 | enable() { 83 | if (this.always_disabled) { 84 | return; 85 | } 86 | if (this.prosemirror_instance) { 87 | this.prosemirror_instance.setProps({editable: true}); 88 | } 89 | super.enable(); 90 | } 91 | 92 | getNumColumns() { 93 | return 6; 94 | } 95 | 96 | setValue(val, initial, from_template) { 97 | const input = super.setValue(val, initial, from_template); 98 | if (input !== undefined && input.changed && this.ckeditor_instance) { 99 | this.prosemirror_instance.updateState({doc: input.value}); 100 | this.refreshWatchedFieldValues(); 101 | this.onChange(true); 102 | } 103 | } 104 | } 105 | 106 | export function patternkitEditorProseMirror($, Drupal, JSONEditor) { 107 | 'use strict'; 108 | Drupal.behaviors.patternkitEditorProseMirror = { 109 | attach: function (context, settings) { 110 | if (!window.JSONEditor) { 111 | return; 112 | } 113 | JSONEditor.defaults.options.drupal_prosemirror = { 114 | ckeditor_config: settings.patternkitEditor.patternkitProseMirrorConfig 115 | }; 116 | JSONEditor.defaults.editors.drupal_prosemirror = DrupalProseMirror; 117 | JSONEditor.defaults.resolvers.unshift(function (schema) { 118 | if (schema.type === 'string' 119 | && schema.format === 'html' 120 | && schema.options 121 | && schema.options.wysiwyg 122 | && settings.patternkitEditor.wysiwygEditorName === 'prosemirror') { 123 | return 'drupal_prosemirror'; 124 | } 125 | }); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /js/patternkit.jsoneditor.quill.es6.js: -------------------------------------------------------------------------------- 1 | /*globals Console:false */ 2 | /*globals Drupal:false */ 3 | /*globals jQuery:false */ 4 | /*globals JSONEditor:false */ 5 | /*globals Quill:false */ 6 | /** 7 | * @file DrupalQuill class. 8 | * 9 | * @external Drupal 10 | * @external jQuery 11 | * @external JSONEditor 12 | * @external Quill 13 | */ 14 | 15 | class DrupalQuill extends JSONEditor.defaults.editors.string { 16 | 17 | build() { 18 | // Override the format when building the base string editor. 19 | this.options.format = 'textarea'; 20 | super.build(); 21 | this.input_type = this.schema.format; 22 | this.input.setAttribute('data-schemaformat', this.input_type); 23 | } 24 | 25 | afterInputReady() { 26 | // Editor options. 27 | // @todo Replace JSONEditor.defaults with this.defaults. 28 | this.options = jQuery.extend({}, JSONEditor.defaults.options.drupal_quill || {}, this.options.drupal_quill || {}); 29 | this.options.quill_config = { 30 | modules: { 31 | toolbar: [ 32 | [{ header: [1, 2, 3, 4, 5, 6, 7, false] }], 33 | ['bold', 'italic', 'underline'], 34 | ['image', 'code-block', 'video'], 35 | [{ 'indent': '-1' }, { 'indent': '+1' }], 36 | [{ 'align': [] }] 37 | ] 38 | }, 39 | placeholder: '', 40 | theme: 'snow' 41 | }; 42 | 43 | this.quill_container = document.createElement('div'); 44 | this.quill_container.style.width = '100%'; 45 | this.quill_container.style.position = 'relative'; 46 | 47 | this.input.style.display = 'none'; 48 | 49 | this.input.parentNode.insertBefore(this.quill_container, this.input); 50 | this.quill_instance = new Quill(this.quill_container, this.options.quill_config); 51 | this.quill_instance.setHTML = (html) => { 52 | this.quill_instance.root.innerHTML = html; 53 | } 54 | this.quill_instance.getHTML = () => { 55 | return this.quill_instance.root.innerHTML; 56 | } 57 | this.quill_instance.setHTML(this.getValue()); 58 | if (this.schema.readOnly || this.schema.readonly || this.schema.template) { 59 | this.quill_instance.disable(); 60 | } 61 | 62 | this.quill_instance.on('text-change', () => { 63 | this.input.value = this.quill_instance.getHTML(); 64 | this.refreshValue(); 65 | // Dirty means display cache is invalidated for string editors. 66 | this.is_dirty = true; 67 | this.onChange(true); 68 | }); 69 | 70 | this.theme.afterInputReady(this.input); 71 | } 72 | 73 | destroy() { 74 | if(this.quill_instance) { 75 | this.quill_instance = null; 76 | } 77 | super.destroy(); 78 | } 79 | 80 | disable(always_disabled) { 81 | if (always_disabled) { 82 | this.always_disabled = true; 83 | } 84 | if (this.quill_instance) { 85 | this.quill_instance.disable(); 86 | } 87 | super.disable(always_disabled); 88 | } 89 | 90 | enable() { 91 | if (this.always_disabled) { 92 | return; 93 | } 94 | if (this.quill_instance) { 95 | this.quill_instance.enable(true); 96 | } 97 | super.enable(); 98 | } 99 | 100 | getNumColumns() { 101 | return 6; 102 | } 103 | 104 | setValue(val, initial, from_template) { 105 | const input = super.setValue(val, initial, from_template); 106 | if (input !== undefined && input.changed && this.quill_instance) { 107 | this.quill_instance.setHTML(input.value); 108 | this.refreshWatchedFieldValues(); 109 | this.onChange(true); 110 | } 111 | } 112 | } 113 | 114 | export function patternkitEditorQuill($, Drupal, JSONEditor) { 115 | 'use strict'; 116 | Drupal.behaviors.patternkitEditorQuill = { 117 | attach: function (context, settings) { 118 | if (!window.JSONEditor) { 119 | return; 120 | } 121 | JSONEditor.defaults.options.drupal_quill = { 122 | quill_config: settings.patternkitEditor.patternkitQuillConfig || {} 123 | }; 124 | JSONEditor.defaults.editors.drupal_quill = DrupalQuill; 125 | JSONEditor.defaults.resolvers.unshift(function (schema) { 126 | if (schema.type === 'string' 127 | && schema.format === 'html' 128 | && schema.options 129 | && schema.options.wysiwyg 130 | && settings.patternkitEditor.wysiwygEditorName == 'quill') { 131 | return 'drupal_quill'; 132 | } 133 | }); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /modules/patternkit_example/README.md: -------------------------------------------------------------------------------- 1 | # PatternKit 2 | Panels-based plugin that wraps a PatternLab library of components. 3 | 4 | This code will parse a pattern library (local or through REST endpoints) to generate a list of "content types" in panels that can be drag/dropped into panel variants. 5 | 6 | If enabled, the configuration screen is simply an iframe wrapper of a simplified version of the PatternLab schema builder. 7 | 8 | When pattern configurations are saved, the template is downloaded locally (to mitigate origin failures and lock in version at time of configuration.) 9 | 10 | Rendered twigs may contain drupal tokens, which are then processed in context. 11 | 12 | ## Installation 13 | Install the patternkit module as usual, and review the important variables below to determine if you would like to change the defaults. 14 | 15 | Install the Twig library into /sites/all/libraries/Twig 16 | ``` 17 | git clone git://github.com/twigphp/Twig.git -b 1.x /tmp/Twig 18 | mv /tmp/Twig/lib/Twig ${DRUPALDIR}/sites/all/libraries/ 19 | rm -rf /tmp/Twig 20 | ``` 21 | 22 | The patternkit module by itself only provides the glue for other modules to present components. Define one by implementing ```hook_patternkit_library``` 23 | 24 | An example implementation follows 25 | ``` 26 | /** 27 | * Implements hook_patternkit_library(). 28 | */ 29 | function webrh_patternkit_library() { 30 | $libraries = array(); 31 | 32 | $namespaces = array( 33 | 'Web RH Patterns' => 'webrh/src/library', 34 | ); 35 | 36 | $module_path = drupal_get_path('module', 'webrh'); 37 | foreach ($namespaces as $namespace => $path) { 38 | $lib_path = $module_path . DIRECTORY_SEPARATOR . $path; 39 | $libraries[] = new PatternkitDrupalTwigLib($namespace, $lib_path); 40 | } 41 | 42 | return $libraries; 43 | } 44 | ``` 45 | 46 | There are two different plugins currently available, 47 | * PatternkitRESTLib 48 | * PatternkitDrupalTwigLib 49 | 50 | Use the former for dynamic REST based components, and the latter for locally sourced. 51 | 52 | ## Important Variables 53 | * ```patternkit_cache_enabled``` - Whether or not the metadata and render cache are enabled. (Disable during development) 54 | * ```patternkit_pl_host``` - The scheme://hostname:port/ of the PatternLab library host. 55 | 56 | ## TODOs 57 | * https://github.com/drupal-pattern-lab/roadmap/issues/8 Solve the problem of mapping Drupal fields to pattern Variables. 58 | * Error handling. 59 | * Better information about available tokens. 60 | * More investigation into the appropriate handling of web component configuration. 61 | * Finalize the CSS/JS management strategy. 62 | More documentation will be added. 63 | 64 | ## Dependencies 65 | [PatternLab](https://github.com/pattern-lab/starterkit-twig-drupal-minimal) 66 | * Schema display support requires: https://github.com/pattern-lab/patternlab-php-core/issues/117 67 | * 68 | 69 | # Proposed V2 Ecosystem 70 | * Exists to solve this issue: https://github.com/drupal-pattern-lab/roadmap/issues/8 71 | * PatternLAB with Restful Extensions provides library + endpoints (Ported from PatternKit) 72 | * PatternKit provides Drupal endpoint consumer (renamed from PKPlugins) 73 | 74 | # Legacy Ecosystem 75 | * PatternKit provides library + RESTful endpoints 76 | * PKPlugins provides Drupal endpoint consumer 77 | -------------------------------------------------------------------------------- /modules/patternkit_example/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal-pattern-lab/patternkit_example", 3 | "license": "MIT", 4 | "authors": [ 5 | { 6 | "name": "Red Hat Inc.", 7 | "email": "drupal@redhat.com" 8 | } 9 | ], 10 | "require": {}, 11 | "config": {}, 12 | "repositories": [], 13 | "scripts": { 14 | "library:build": [ 15 | ". ~/.profile && nvm install && nvm use", 16 | "npm install && npx gulp" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true 5 | }, 6 | "globals": { 7 | "Drupal": true, 8 | "jQuery": true 9 | }, 10 | "rules": { 11 | // Errors. 12 | "array-bracket-spacing": [2, "never"], 13 | "block-scoped-var": 2, 14 | "brace-style": [2, "stroustrup", {"allowSingleLine": true}], 15 | "comma-dangle": [2, "never"], 16 | "comma-spacing": 2, 17 | "comma-style": [2, "last"], 18 | "computed-property-spacing": [2, "never"], 19 | "curly": [2, "all"], 20 | "eol-last": 2, 21 | "eqeqeq": [2, "smart"], 22 | "guard-for-in": 2, 23 | "indent": [2, 2, {"SwitchCase": 1}], 24 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 25 | "keyword-spacing": [2, {"before": true, "after": true}], 26 | "linebreak-style": [2, "unix"], 27 | "lines-around-comment": [2, {"beforeBlockComment": true, "afterBlockComment": false}], 28 | "new-parens": 2, 29 | "no-array-constructor": 2, 30 | "no-caller": 2, 31 | "no-catch-shadow": 2, 32 | "no-eval": 2, 33 | "no-extend-native": 2, 34 | "no-extra-bind": 2, 35 | "no-extra-parens": [2, "functions"], 36 | "no-implied-eval": 2, 37 | "no-iterator": 2, 38 | "no-label-var": 2, 39 | "no-labels": 2, 40 | "no-lone-blocks": 2, 41 | "no-loop-func": 2, 42 | "no-multi-spaces": 2, 43 | "no-multi-str": 2, 44 | "no-native-reassign": 2, 45 | "no-nested-ternary": 2, 46 | "no-new-func": 2, 47 | "no-new-object": 2, 48 | "no-new-wrappers": 2, 49 | "no-octal-escape": 2, 50 | "no-process-exit": 2, 51 | "no-proto": 2, 52 | "no-return-assign": 2, 53 | "no-script-url": 2, 54 | "no-sequences": 2, 55 | "no-shadow-restricted-names": 2, 56 | "no-spaced-func": 2, 57 | "no-trailing-spaces": 2, 58 | "no-undef-init": 2, 59 | "no-undefined": 2, 60 | "no-unused-expressions": 2, 61 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 62 | "no-with": 2, 63 | "object-curly-spacing": [2, "never"], 64 | "one-var": [2, "never"], 65 | "quote-props": [2, "consistent-as-needed"], 66 | "quotes": [2, "single", "avoid-escape"], 67 | "semi": [2, "always"], 68 | "semi-spacing": [2, {"before": false, "after": true}], 69 | "space-before-blocks": [2, "always"], 70 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 71 | "space-in-parens": [2, "never"], 72 | "space-infix-ops": 2, 73 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 74 | "spaced-comment": [2, "always"], 75 | "strict": 2, 76 | "yoda": [2, "never"], 77 | // Warnings. 78 | "max-nested-callbacks": [1, 3], 79 | "valid-jsdoc": [1, { 80 | "prefer": { 81 | "returns": "return", 82 | "property": "prop" 83 | }, 84 | "requireReturn": false 85 | }] 86 | } 87 | } -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/.nvmrc: -------------------------------------------------------------------------------- 1 | 8.9.4 2 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/.patternlabrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/.patternlabrc -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # Documentation for the sass-lint Linters is available at: 2 | # https://github.com/sasstools/sass-lint/tree/master/docs/rules 3 | 4 | options: 5 | formatter: stylish 6 | merge-default-rules: true 7 | rules: 8 | extends-before-mixins: 2 9 | extends-before-declarations: 2 10 | placeholder-in-extend: 2 11 | mixins-before-declarations: 12 | - 2 13 | one-declaration-per-line: 2 14 | empty-line-between-blocks: 15 | - 2 16 | - ignore-single-line-rulesets: false 17 | single-line-per-selector: 2 18 | 19 | no-attribute-selectors: 0 20 | no-color-hex: 2 21 | no-color-keywords: 2 22 | no-color-literals: 2 23 | no-combinators: 0 24 | no-debug: 2 25 | no-disallowed-properties: 2 26 | no-duplicate-properties: 2 27 | no-empty-rulesets: 2 28 | no-extends: 0 29 | no-ids: 2 30 | no-important: 2 31 | no-invalid-hex: 2 32 | no-mergeable-selectors: 2 33 | no-misspelled-properties: 34 | - 2 35 | - extra-properties: 36 | - '*font-family' 37 | - '*height' 38 | - 'interpolation-mode' 39 | - '*margin-left' 40 | - '*vertical-align' 41 | - '*width' 42 | no-qualifying-elements: 0 43 | no-trailing-whitespace: 2 44 | no-trailing-zero: 2 45 | no-transition-all: 2 46 | no-universal-selectors: 0 47 | no-url-protocols: 2 48 | no-warn: 2 49 | property-units: 0 50 | 51 | force-attribute-nesting: 0 52 | force-element-nesting: 0 53 | force-pseudo-nesting: 0 54 | 55 | class-name-format: 56 | - 2 57 | - 58 | convention: hyphenatedbem 59 | function-name-format: 2 60 | id-name-format: 2 61 | mixin-name-format: 62 | - 2 63 | - convention: hyphenatedbem 64 | placeholder-name-format: 65 | - 2 66 | - convention: hyphenatedbem 67 | variable-name-format: 2 68 | 69 | attribute-quotes: 2 70 | bem-depth: 0 71 | border-zero: 2 72 | brace-style: 73 | - 2 74 | - style: stroustrup 75 | - allow-single-line: false 76 | clean-import-paths: 2 77 | empty-args: 0 78 | hex-length: 2 79 | hex-notation: 2 80 | indentation: 2 81 | leading-zero: 2 82 | nesting-depth: 83 | - 2 84 | - max-depth: 4 85 | property-sort-order: 2 86 | pseudo-element: 0 87 | quotes: 2 88 | shorthand-values: 2 89 | url-quotes: 2 90 | variable-for-property: 2 91 | zero-unit: 2 92 | 93 | space-after-comma: 2 94 | space-before-colon: 2 95 | space-after-colon: 2 96 | space-before-brace: 2 97 | space-before-bang: 2 98 | space-after-bang: 2 99 | space-between-parens: 0 100 | space-around-operator: 0 101 | 102 | trailing-semicolon: 2 103 | final-newline: 2 104 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/README.md: -------------------------------------------------------------------------------- 1 | # Patternkit Pattern Library 2 | This is an example pattern library meant as reference to get you started with creating your own. 3 | 4 | ## Getting Started 5 | Clone this repo, and then init the dev tools. 6 | 7 | ```bash 8 | git clone https://github.com/drupal-pattern-lab/patternkit_patterns.git patternkit 9 | nvm use 10 | npm i 11 | npm run watch 12 | ``` 13 | 14 | ## Requirements 15 | - [Node.js](https://nodejs.org/en/) 16 | 17 | ## Visual Language 18 | - [Material Design](https://material.io/) 19 | 20 | ## Design System 21 | - [Atomic Design](http://atomicdesign.bradfrost.com/table-of-contents/) 22 | 23 | ## Standards 24 | - https://github.com/cybtachyon/twig-standards 25 | 26 | ## Contribution Guidelines 27 | All contributions will need to be vetted by a maintainer before being committed. Make a Pull Request today! 28 | 29 | ## Maintainers 30 | - Erik "CloudNyne" Baldwin 31 | - Derek "tachyon" Reese 32 | - Jason "SiliconValet" Smith 33 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/dist/patternkit.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/dist/patternkit.css -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/dist/patternkit.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/dist/patternkit.min.js -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patternkit", 3 | "version": "1.0.0", 4 | "description": "Example Pattern Library", 5 | "main": "Gulpfile.js", 6 | "scripts": { 7 | "test": "watch" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/drupal-pattern-lab/patternkit_patterns.git" 12 | }, 13 | "keywords": [ 14 | "pattern" 15 | ], 16 | "author": "Derek Reese", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/drupal-pattern-lab/patternkit_patterns/issues" 20 | }, 21 | "homepage": "https://github.com/drupal-pattern-lab/patternkit_patterns#readme", 22 | "devDependencies": { 23 | "gulp-cli": "^2.0.1", 24 | "node-sass": "^4.13.1", 25 | "parcel-bundler": "^1.5.1", 26 | "sass-lint": "^1.12.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/dist/example.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example/dist/example.css -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/dist/example.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example/dist/example.min.js -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | import './src/example.scss'; 5 | import example from './dist/example'; 6 | 7 | var element = Object.create(HTMLElement.prototype); 8 | element.createdCallback = function () {}; 9 | element.attachedCallback = function () {}; 10 | element.detachedCallback = function () {}; 11 | element.attributeChangedCallback = function (attr, oldVal, newVal) {}; 12 | document.registerElement('example', { 13 | prototype: element 14 | }); 15 | }()); 16 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "category": "atom", 4 | "title": "Example", 5 | "type": "object", 6 | "format": "grid", 7 | "properties": { 8 | "text": { 9 | "title": "Text", 10 | "type": "string", 11 | "options": { 12 | "grid_columns": 4 13 | } 14 | }, 15 | "formatted_text": { 16 | "title": "Formatted Text", 17 | "type": "string", 18 | "format": "html", 19 | "options": { 20 | "wysiwyg": true 21 | } 22 | }, 23 | "image_url": { 24 | "title": "Image URL", 25 | "type": "string", 26 | "format": "image", 27 | "options": { 28 | "grid_columns": 6 29 | } 30 | }, 31 | "hidden": { 32 | "title": "hidden", 33 | "type": "string" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.md: -------------------------------------------------------------------------------- 1 | This is an example Web Component, implemented as a Twig UI Pattern. 2 | 3 | It includes the following files: 4 | 5 | ## example.json 6 | The JSON schema file delineates the schema of the pattern. In some cases, libraries may generate these from the Twig Docblock of the pattern Twig template file. 7 | 8 | ## example.md 9 | This file. The text here should appear as pattern documentation in any Style Guide, Pattern Library, or pattern developer tool. 10 | 11 | This documentation often will include Style Rulesets, brand guidelines, designer intention, as well as usage advice. 12 | 13 | ## example.scss 14 | Optional SASS to be compiled to CSS 15 | 16 | ## example.ts 17 | Optional Typescript file to be transpiled to the final JS module. In some libraries, vanilla JS or other variants are used. 18 | 19 | ## example.twig 20 | The HTML template for the pattern. Some libraries may also use Nunjucks, JSX, or Handlebars. 21 | 22 | ## example.yml 23 | Sample data for the pattern. This can be used to generate demo sites, style guide examples, or pre-fill schema editors. 24 | 25 | ## index.js 26 | The Web Component entry point. 27 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.scss -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.ts -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.twig: -------------------------------------------------------------------------------- 1 | Sample twig template. 2 | 3 |
{{- text|striptags('') -}}
4 | 5 |
{{ formatted_text|raw }}
6 | 7 | {% if image_url %} 8 |
9 | 10 |
This is the image for URL {{ image_url }}.
11 |
12 | {% endif %} 13 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.yml: -------------------------------------------------------------------------------- 1 | text: Sample text 2 | formatted_text: Sample formatted text.
3 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/dist/example_filtered.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/dist/example_filtered.css -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/dist/example_filtered.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/dist/example_filtered.min.js -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | import './src/example_filtered.scss'; 5 | import example_filtered from './dist/example_filtered'; 6 | 7 | var element = Object.create(HTMLElement.prototype); 8 | element.createdCallback = function () {}; 9 | element.attachedCallback = function () {}; 10 | element.detachedCallback = function () {}; 11 | element.attributeChangedCallback = function (attr, oldVal, newVal) {}; 12 | document.registerElement('example_filtered', { 13 | prototype: element 14 | }); 15 | }()); 16 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "category": "atom", 4 | "title": "Example with filtered content", 5 | "type": "object", 6 | "format": "grid", 7 | "properties": { 8 | "text": { 9 | "title": "Text", 10 | "type": "string", 11 | "options": { 12 | "grid_columns": 4 13 | } 14 | }, 15 | "formatted_text": { 16 | "title": "Formatted Text", 17 | "type": "string", 18 | "description": "Only links, bold/strong, and italic/emphasis tags are allowed (assuming CKEditor is the wysiwyg plugin). You can use any attributes on these elements (including class, style, and other attributes), except ones prefixed with \"data-\". This field's schema uses the \"disallowedContent\" rule to instruct CKEditor to strip these attributes. However, there is no server-side process that strips them. See Patternkit's README for details.", 19 | "format": "html", 20 | "options": { 21 | "wysiwyg": true, 22 | "allowedContent": "a b strong em i[*](*){*}", 23 | "disallowedContent": "*[data-*]" 24 | } 25 | }, 26 | "hidden": { 27 | "title": "hidden", 28 | "type": "string" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.md: -------------------------------------------------------------------------------- 1 | This is an example Web Component showing content filtered by CKEditor, implemented as a Twig UI Pattern. 2 | 3 | It includes the following files: 4 | 5 | ## example_filtered.json 6 | The JSON schema file delineates the schema of the pattern. In some cases, libraries may generate these from the Twig Docblock of the pattern Twig template file. 7 | 8 | ## example_filtered.md 9 | This file. The text here should appear as pattern documentation in any Style Guide, Pattern Library, or pattern developer tool. 10 | 11 | This documentation often will include Style Rulesets, brand guidelines, designer intention, as well as usage advice. 12 | 13 | ## example_filtered.scss 14 | Optional SASS to be compiled to CSS 15 | 16 | ## example_filtered.ts 17 | Optional Typescript file to be transpiled to the final JS module. In some libraries, vanilla JS or other variants are used. 18 | 19 | ## example_filtered.twig 20 | The HTML template for the pattern. Some libraries may also use Nunjucks, JSX, or Handlebars. 21 | 22 | ## example_filtered.yml 23 | Sample data for the pattern. This can be used to generate demo sites, style guide example_filtereds, or pre-fill schema editors. 24 | 25 | ## index.js 26 | The Web Component entry point. 27 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.scss -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.ts -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.twig: -------------------------------------------------------------------------------- 1 | Sample twig template with filtered content. 2 | 3 |
{{- text|striptags('') -}}
4 | 5 |

The following block allows only tags <a><b><strong><em><i>, including any attributes on them 6 | (including <style>).

7 | 8 |
{{ formatted_text|striptags('')|raw }}
9 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_filtered/src/example_filtered.yml: -------------------------------------------------------------------------------- 1 | text: Sample text 2 | formatted_text: Sample formatted text with a link.
3 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/dist/example_ref.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_ref/dist/example_ref.css -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/dist/example_ref.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_ref/dist/example_ref.min.js -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | import './src/example.scss'; 5 | import example from './dist/example'; 6 | 7 | var element = Object.create(HTMLElement.prototype); 8 | element.createdCallback = function () {}; 9 | element.attachedCallback = function () {}; 10 | element.detachedCallback = function () {}; 11 | element.attributeChangedCallback = function (attr, oldVal, newVal) {}; 12 | document.registerElement('example', { 13 | prototype: element 14 | }); 15 | }()); 16 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "category": "atom", 4 | "title": "Example with Reference", 5 | "type": "object", 6 | "format": "grid", 7 | "properties": { 8 | "text": { 9 | "$ref": "@patternkit/atoms/example_ref/../example/src/example.json#/properties/text" 10 | }, 11 | "nested_reference": { 12 | "$ref": "@patternkit/atoms/example_ref/../example/src/example.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.md: -------------------------------------------------------------------------------- 1 | This is an example Web Component with a Reference and include, implemented as a Twig UI Pattern. 2 | 3 | It includes the following files: 4 | 5 | ## example_ref.json 6 | The JSON schema file delineates the schema of the pattern. In some cases, libraries may generate these from the Twig Docblock of the pattern Twig template file. 7 | 8 | ## example_ref.md 9 | This file. The text here should appear as pattern documentation in any Style Guide, Pattern Library, or pattern developer tool. 10 | 11 | This documentation often will include Style Rulesets, brand guidelines, designer intention, as well as usage advice. 12 | 13 | ## example_ref.scss 14 | Optional SASS to be compiled to CSS 15 | 16 | ## example_ref.ts 17 | Optional Typescript file to be transpiled to the final JS module. In some libraries, vanilla JS or other variants are used. 18 | 19 | ## example_ref.twig 20 | The HTML template for the pattern. Some libraries may also use Nunjucks, JSX, or Handlebars. 21 | 22 | ## example_ref.yml 23 | Sample data for the pattern. This can be used to generate demo sites, style guide examples, or pre-fill schema editors. 24 | 25 | ## index.js 26 | The Web Component entry point. 27 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.scss -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drupal-pattern-lab/patternkit/2ec7389a5d8b088d27845f0ad9f6cd215446f697/modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.ts -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.twig: -------------------------------------------------------------------------------- 1 | Example twig template with include and reference. 2 | 3 |
{{- text|striptags('') -}}
4 | 5 | {% include '@patternkit/atoms/example/src/example.twig' with nested_reference %} 6 | -------------------------------------------------------------------------------- /modules/patternkit_example/lib/patternkit/src/atoms/example_ref/src/example_ref.yml: -------------------------------------------------------------------------------- 1 | text: Sample text, included 2 | -------------------------------------------------------------------------------- /modules/patternkit_example/patternkit_example.info.yml: -------------------------------------------------------------------------------- 1 | name: Patternkit Example 2 | description: Adds Patternkit example patterns. 3 | type: module 4 | package: Presentation Framework 5 | core: 8.x 6 | core_version_requirement: ^8 || ^9 7 | dependencies: 8 | - patternkit 9 | -------------------------------------------------------------------------------- /modules/patternkit_example/patternkit_example.libraries.yml: -------------------------------------------------------------------------------- 1 | patternkit: 2 | version: VERSION 3 | css: 4 | theme: 5 | lib/patternkit/dist/patternkit.css: {} 6 | js: 7 | lib/patternkit/dist/patternkit.min.js: {} 8 | patterns: 9 | lib/patternkit/src: {plugin: twig} 10 | -------------------------------------------------------------------------------- /modules/patternkit_example/patternkit_example.module: -------------------------------------------------------------------------------- 1 | = 11"] 8 | }, 9 | "forceAllTransforms": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | js/globals**.js** 3 | node_modules 4 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | The Patternkit Media Library Support module allows you to use Drupal's media 4 | library browser to select images for patterns. 5 | 6 | This module will add a button to image fields. You can click this button to open 7 | the media library browser in a modal window. 8 | 9 | ## Installation 10 | 11 | Enable this module (e.g., `drush en patternkit_media_library`). 12 | 13 | ## Configuration 14 | 15 | You can configure this module at path 16 | `/admin/config/user-interface/patternkit/media-library`. 17 | 18 | ### Bundled styles 19 | 20 | You can choose whether to load supplemental styles for the media library 21 | browser. This feature relies on templates from [Media Library Theme 22 | Reset](https://www.drupal.org/project/media_library_theme_reset). 23 | 24 | ## Example 25 | 26 | This module adds the media library browser button to patterns where both: 27 | 28 | * `type` = `string` 29 | * `format` = `image` 30 | 31 | See [example.json](modules/patternkit_example/lib/patternkit/src/atoms/example/src/example.json). 32 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal-pattern-lab/patternkit_media_library", 3 | "license": "MIT", 4 | "authors": [ 5 | { 6 | "name": "Red Hat Inc.", 7 | "email": "drupal@redhat.com" 8 | } 9 | ], 10 | "require": {}, 11 | "config": {}, 12 | "repositories": [], 13 | "scripts": { 14 | "js:build": [ 15 | ". ~/.profile && nvm install && nvm use", 16 | "npm install && npx gulp" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/config/install/patternkit_media_library.settings.yml: -------------------------------------------------------------------------------- 1 | use_styles: false 2 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/config/schema/patternkit_media_library.schema.yml: -------------------------------------------------------------------------------- 1 | patternkit_media_library.settings: 2 | type: config_object 3 | label: 'Patternkit Media Library Module Settings' 4 | mapping: 5 | use_styles: 6 | type: boolean 7 | label: 'Boolean that sets whether to load bundled styles for the media library browser.' 8 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/css/media-library-modal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Supplies decent defaults for the media library interface. 4 | */ 5 | 6 | /* Renders the grid mode as a grid. */ 7 | .patternkit-media-library .media-library-views-form__rows { 8 | display: flex; 9 | flex-wrap: wrap; 10 | } 11 | .patternkit-media-library .media-library-item--grid { 12 | /* Displays 3 items per row because the modal's width is max 900px. */ 13 | width: calc(33% - 1em); 14 | margin: 0.5em; 15 | padding: 0.5em; 16 | border: 2px solid #dbdbdb; 17 | border-radius: 2px; 18 | } 19 | .patternkit-media-library .media-library-item--grid.is-hover, 20 | .patternkit-media-library .media-library-item--grid.checked { 21 | border-color: #5a8bed; 22 | } 23 | .patternkit-media-library .media-library-item--grid .media-library-item__click-to-select-trigger { 24 | cursor: pointer; 25 | } 26 | 27 | /* Ensures each Views exposed form is displayed inline. */ 28 | .patternkit-media-library .form--inline, 29 | .patternkit-media-library .views-exposed-form { 30 | display: flex; 31 | align-items: end; 32 | margin-bottom: 1em; 33 | } 34 | .patternkit-media-library .form--inline > *, 35 | .patternkit-media-library .views-exposed-form > * { 36 | margin: 0 1em 0 0; 37 | } 38 | 39 | /* Gives the Views pager some sane defaults. */ 40 | .patternkit-media-library .media-library-view nav[role="navigation"] ul { 41 | display: flex; 42 | flex-wrap: wrap; 43 | align-items: flex-end; 44 | justify-content: center; 45 | margin: 1em 0; 46 | list-style: none; 47 | text-align: center; 48 | font-weight: bold; 49 | } 50 | .patternkit-media-library .media-library-view nav[role="navigation"] ul > li { 51 | margin: 0 0.5em; 52 | } 53 | 54 | .patternkit-media-library .views-display-link.is-active { 55 | font-weight: 700; 56 | } 57 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babelify = require('babelify'); 3 | const browserify = require("browserify"); 4 | const buffer = require('vinyl-buffer'); 5 | const source = require('vinyl-source-stream'); 6 | 7 | const gulpConfig = { 8 | src: './js/**es6.js', 9 | dist: './js', 10 | main: 'patternkit.jsoneditor.media_library.es6.js' 11 | }; 12 | 13 | const compileEs6 = function () { 14 | return browserify(gulpConfig.dist + '/' + gulpConfig.main) 15 | .transform(babelify) 16 | .bundle() 17 | .pipe(source(gulpConfig.main.replace('.es6', ''))) 18 | .pipe(buffer()) 19 | .pipe(gulp.dest(gulpConfig.dist)); 20 | }; 21 | 22 | gulp.task('compile:es6', compileEs6); 23 | 24 | gulp.task('watch', function() { 25 | return gulp.watch(gulpConfig.src, 'compile:es6'); 26 | }); 27 | 28 | gulp.task('default', compileEs6); 29 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patternkit-media-library", 3 | "version": "1.0.0-alpha", 4 | "description": "Media Library support for Patternkit.", 5 | "main": "js/patternkit.jsoneditor.media_library.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/drupal-pattern-lab/patternkit.git" 9 | }, 10 | "author": "cybtachyon", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/drupal-pattern-lab/drupal-pattern-lab/issues" 14 | }, 15 | "homepage": "https://github.com/drupal-pattern-lab/drupal-pattern-lab#readme", 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "@babel/cli": "^7.8.4", 19 | "@babel/core": "^7.8.7", 20 | "@babel/preset-env": "^7.8.7", 21 | "babelify": "^10.0.0", 22 | "browserify": "^16.5.0", 23 | "gulp": "^4.0.2", 24 | "gulp-concat": "^2.6.1", 25 | "gulp-rename": "^2.0.0", 26 | "gulp-replace": "^1.0.0", 27 | "gulp-sourcemaps": "^2.6.5", 28 | "vinyl-buffer": "^1.0.1", 29 | "vinyl-source-stream": "^2.0.0" 30 | }, 31 | "scripts": { 32 | "build": "gulp", 33 | "dev": "gulp", 34 | "eslint": "eslint -c ./.eslintrc js/src/**/*.es6.js", 35 | "eslint.fix": "eslint -c ./.eslintrc js/src/**/*.es6.js --fix", 36 | "postversion": "git push && git push --tags", 37 | "serve-test": "http-server --p 9001", 38 | "watch": "gulp watch" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.info.yml: -------------------------------------------------------------------------------- 1 | name: Patternkit Media Library Support 2 | description: Adds Media Library Support to upload fields. 3 | type: module 4 | package: Presentation Framework 5 | core: 8.x 6 | core_version_requirement: ^8 || ^9 7 | configure: patternkit.settings 8 | dependencies: 9 | - drupal:media_library 10 | - drupal:patternkit 11 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.install: -------------------------------------------------------------------------------- 1 | moduleExists('entity_browser'); 15 | $media_entity_browser_installed = \Drupal::moduleHandler()->moduleExists('media_entity_browser'); 16 | if (!$entity_browser_installed || !$media_entity_browser_installed) { 17 | $requirements['patternkit_media_library_recommended_modules'] = [ 18 | 'title' => t('Patternkit Media Library Recommended Modules'), 19 | 'description' => t('Patternkit Media Library recommends installing Entity Browser and Media Entity Browser for the best Media Library Experience.'), 20 | 'severity' => REQUIREMENT_INFO, 21 | ]; 22 | } 23 | 24 | return $requirements; 25 | } 26 | 27 | /** 28 | * Sets default value for new config item 'patternkit_media_library.settings'. 29 | */ 30 | function patternkit_media_library_update_8001(&$sandbox) { 31 | $config_key = 'patternkit_media_library.settings'; 32 | 33 | $config_current = \Drupal::configFactory()->getEditable($config_key); 34 | // Installs the default config only if this config is not already set. 35 | if (is_null($config_current->get('use_styles'))) { 36 | /** @var \Drupal\Core\Config\ConfigInstaller $config_installer_service */ 37 | $config_installer_service = \Drupal::service('config.installer'); 38 | $config_installer_service->installDefaultConfig('module', 'patternkit_media_library'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.libraries.yml: -------------------------------------------------------------------------------- 1 | patternkit.jsoneditor.media_library: 2 | version: VERSION 3 | js: 4 | js/patternkit.jsoneditor.media_library.js: {} 5 | dependencies: 6 | - core/drupalSettings 7 | - core/jquery 8 | - media_library/ui 9 | - patternkit/jsoneditor 10 | 11 | media_library_modal: 12 | version: VERSION 13 | css: 14 | theme: 15 | css/media-library-modal.css: {} 16 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.links.task.yml: -------------------------------------------------------------------------------- 1 | patternkit.settings.media_library: 2 | title: 'Media Library Settings' 3 | route_name: patternkit.settings.media_library 4 | base_route: patternkit.settings 5 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.module: -------------------------------------------------------------------------------- 1 | toString(); 18 | array_unshift($form_attached['library'], 'patternkit_media_library/patternkit.jsoneditor.media_library'); 19 | 20 | $config = \Drupal::config('patternkit_media_library.settings'); 21 | if ($config->get('use_styles')) { 22 | $form_attached['library'][] = 'patternkit_media_library/media_library_modal'; 23 | } 24 | 25 | $form['settings']['configuration']['#attached'] = $form_attached; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.routing.yml: -------------------------------------------------------------------------------- 1 | patternkit.media_library: 2 | path: '/patternkit_media_library' 3 | defaults: 4 | _controller: '\Drupal\patternkit_media_library\Controller\PatternkitMediaLibraryController::mediaLibrary' 5 | requirements: 6 | _permission: 'administer blocks,view media' 7 | 8 | patternkit.settings.media_library: 9 | path: '/admin/config/user-interface/patternkit/media-library' 10 | defaults: 11 | _form: '\Drupal\patternkit_media_library\Form\MediaLibrarySettingsForm' 12 | _title: 'Patternkit Media Library settings' 13 | _description: 'Configure Patternkit Media Library Support.' 14 | options: 15 | _admin_route: TRUE 16 | requirements: 17 | _permission: 'access administration pages' 18 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/patternkit_media_library.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | patternkit.opener.jsonlibrary: 3 | class: Drupal\patternkit_media_library\MediaLibraryJSONLibraryOpener 4 | arguments: ['@entity_type.manager', '@file_url_generator'] 5 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/src/Controller/PatternkitMediaLibraryController.php: -------------------------------------------------------------------------------- 1 | query; 28 | $ml_state = MediaLibraryState::create( 29 | $query->get('media_library_opener_id'), 30 | $query->get('media_library_allowed_types', []), 31 | $query->get('media_library_selected_type'), 32 | $query->get('media_library_remaining'), 33 | $query->get('media_library_opener_parameters', []) 34 | ); 35 | if (!\Drupal::hasService('media_library.ui_builder')) { 36 | throw new ServiceNotFoundException('media_library.ui_builder'); 37 | } 38 | /** @var \Drupal\media_library\MediaLibraryUiBuilder $ml_ui_builder */ 39 | $ml_ui_builder = \Drupal::service('media_library.ui_builder'); 40 | 41 | return $ml_ui_builder->buildUi($ml_state); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/src/Form/MediaLibrarySettingsForm.php: -------------------------------------------------------------------------------- 1 | config(static::SETTINGS); 27 | 28 | $form['use_styles'] = [ 29 | '#type' => 'checkbox', 30 | '#title' => t('Use bundled styles for the media library modal window'), 31 | '#description' => t('If checked, then Patternkit will load some styles for 32 | the media library modal window.
This feature relies on templates 33 | provided by 34 | Media Library Theme Reset, so enable that module if you want to use 35 | these styles.'), 36 | '#default_value' => $config->get('use_styles') ?? FALSE, 37 | ]; 38 | 39 | return parent::buildForm($form, $form_state); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function getEditableConfigNames() :array { 46 | return [static::SETTINGS]; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getFormId() :string { 53 | return 'patternkit_media_library_settings_form'; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function submitForm(array &$form, FormStateInterface $form_state): void { 60 | $form_values = $form_state->getValues(); 61 | $config = $this->config(self::SETTINGS); 62 | 63 | $config->set('use_styles', (bool) $form_values['use_styles']); 64 | $config->save(); 65 | 66 | parent::submitForm($form, $form_state); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /modules/patternkit_media_library/src/MediaLibraryJSONLibraryOpener.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 43 | $this->fileUrlGenerator = $file_url_generator; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function checkAccess(MediaLibraryState $state, AccountInterface $account) { 50 | return AccessResult::allowed(); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getSelectionResponse(MediaLibraryState $state, array $selected_ids): AjaxResponse { 57 | $response = new AjaxResponse(); 58 | 59 | $parameters = $state->getOpenerParameters(); 60 | if (empty($parameters['field_widget_id'])) { 61 | throw new \InvalidArgumentException('field_widget_id parameter is missing.'); 62 | } 63 | 64 | $widget_id = $parameters['field_widget_id']; 65 | if (!$mid = reset($selected_ids)) { 66 | return $response; 67 | } 68 | try { 69 | /** @var \Drupal\media\Entity\Media $media */ 70 | $media = $this->entityTypeManager->getStorage('media')->load($mid); 71 | $fid = $media->getSource()->getSourceFieldValue($media); 72 | $file = $this->entityTypeManager->getStorage('file')->load($fid); 73 | } 74 | catch (\Exception $exception) { 75 | return $response; 76 | } 77 | if (!$file) { 78 | return $response; 79 | } 80 | try { 81 | if ($file->hasLinkTemplate('canonical')) { 82 | $url = $file->toUrl()->setAbsolute(FALSE); 83 | } 84 | elseif ($file->access('download')) { 85 | $url = $this->fileUrlGenerator->generateString($file->getFileUri()); 86 | } 87 | else { 88 | $url = $file->label(); 89 | } 90 | $response->addCommand(new PatternkitEditorUpdateCommand($widget_id, $url)); 91 | } 92 | catch (\Exception $exception) { 93 | return $response; 94 | } 95 | 96 | return $response; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patternkit", 3 | "version": "1.0.0-alpha", 4 | "description": "Drupal module enabling drag and drop of templates into layouts.", 5 | "main": "js/patternkit.jsoneditor.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/drupal-pattern-lab/patternkit.git" 9 | }, 10 | "author": "cybtachyon", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/drupal-pattern-lab/patternkit/issues" 14 | }, 15 | "homepage": "https://github.com/drupal-pattern-lab/patternkit#readme", 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "@babel/cli": "^7.12.1", 19 | "@babel/core": "^7.10.3", 20 | "@babel/preset-env": "^7.10.3", 21 | "@json-editor/json-editor": "^2.7.0", 22 | "ajv": "^6.12.2", 23 | "babelify": "^10.0.0", 24 | "browserify": "^16.5.1", 25 | "gulp": "^4.0.2", 26 | "gulp-prefix-css": "0.0.4", 27 | "gulp-rename": "^2.0.0", 28 | "gulp-replace": "^1.0.0", 29 | "gulp-sourcemaps": "^2.6.5", 30 | "handlebars": "^4.7.7", 31 | "prosemirror-example-setup": "^1.1.2", 32 | "prosemirror-model": "^1.12.0", 33 | "prosemirror-schema-basic": "^1.1.2", 34 | "prosemirror-schema-list": "^1.1.4", 35 | "prosemirror-state": "^1.3.3", 36 | "prosemirror-view": "^1.16.1", 37 | "vinyl-buffer": "^1.0.1", 38 | "vinyl-source-stream": "^2.0.0" 39 | }, 40 | "scripts": { 41 | "build": "gulp", 42 | "dev": "gulp", 43 | "eslint": "eslint -c ./.eslintrc js/src/**/*.es6.js", 44 | "eslint.fix": "eslint -c ./.eslintrc js/src/**/*.es6.js --fix", 45 | "postversion": "git push && git push --tags", 46 | "serve-test": "http-server --p 9001", 47 | "watch": "gulp watch" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /patternkit.api.php: -------------------------------------------------------------------------------- 1 | moduleExists('patternkit_example') ? Url::fromRoute('help.page', ['name' => 'patternkit_example'])->toString() : '#'; 18 | $patternkit_media_library = \Drupal::moduleHandler()->moduleExists('patternkit_media_library') ? Url::fromRoute('help.page', ['name' => 'patternkit_media_library'])->toString() : '#'; 19 | 20 | $output = <<About 22 |

The Patternkit module loads your templates, patterns, and components into Drupal as blocks where you can then add them to your pages and layouts. You can configure them with a JSON file with an identical filename in the same directory as the Twig template.

\ 23 |

This allows Layout Builder, Page Manager, Drupal blocks layout, or anything that uses blocks, to function as a custom page-builder app.

24 |

Site builders can add Drupal tokens to Pattern blocks instead of trying to pick through Drupal variables in a template. These are displayed from assigned Context such as node, user, and language.

25 | 26 |

Uses

27 |
28 |
Loading a library of patterns to use with Layout Builder
29 |
Users with the Administer blocks permission can add Patternkit blocks to entity layouts.
30 |
Loading a directory of patterns to use with the Block Layout
31 |
Users with the Administer blocks permission can add Patternkit blocks to the site block layout.
32 |
33 | HTML; 34 | // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString 35 | return t($output, [ 36 | ':patternkit_example' => $patternkit_example, 37 | ':patternkit_media_library' => $patternkit_media_library, 38 | ]); 39 | 40 | default: 41 | return NULL; 42 | } 43 | } 44 | 45 | /** 46 | * Implements hook_theme(). 47 | */ 48 | function patternkit_theme($existing, $type, $theme, $path): array { 49 | return [ 50 | 'patternkit_add_list' => [ 51 | 'variables' => ['content' => NULL], 52 | ], 53 | ]; 54 | } 55 | -------------------------------------------------------------------------------- /patternkit.polyfil.php: -------------------------------------------------------------------------------- 1 | $v) { 25 | ++$i; 26 | if ($k !== $i) { 27 | return FALSE; 28 | } 29 | } 30 | return TRUE; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /patternkit.routing.yml: -------------------------------------------------------------------------------- 1 | entity.patternkit_block.collection: 2 | path: '/admin/structure/block/patternkit' 3 | defaults: 4 | _title: 'Patternkit block library' 5 | _entity_list: 'patternkit_block' 6 | requirements: 7 | _permission: 'administer blocks' 8 | 9 | entity.patternkit_block.canonical: 10 | path: '/patternkit_block/{patternkit_block}' 11 | defaults: 12 | _entity_form: 'patternkit_block.edit' 13 | options: 14 | _admin_route: TRUE 15 | requirements: 16 | _permission: 'administer blocks' 17 | patternkit_block: \d+ 18 | 19 | entity.patternkit_block.edit_form: 20 | path: '/patternkit_block/{patternkit_block}' 21 | defaults: 22 | _entity_form: 'patternkit_block.edit' 23 | options: 24 | _admin_route: TRUE 25 | requirements: 26 | _permission: 'administer blocks' 27 | patternkit_block: \d+ 28 | 29 | entity.patternkit_block.delete_form: 30 | path: '/patternkit_block/{patternkit_block}/delete' 31 | defaults: 32 | _entity_form: 'patternkit_block.delete' 33 | _title: 'Delete' 34 | options: 35 | _admin_route: TRUE 36 | requirements: 37 | _permission: 'administer blocks' 38 | patternkit_block: \d+ 39 | 40 | patternkit.add_form: 41 | path: '/patternkit_block/add/{pattern_id}' 42 | defaults: 43 | _controller: '\Drupal\patternkit\Controller\PatternkitController::addForm' 44 | _title_callback: '\Drupal\patternkit\Controller\PatternkitController::getAddFormTitle' 45 | options: 46 | _admin_route: TRUE 47 | requirements: 48 | _permission: 'administer blocks' 49 | 50 | patternkit.add_page: 51 | path: '/patternkit_block/add' 52 | defaults: 53 | _controller: '\Drupal\patternkit\Controller\PatternkitController::add' 54 | _title: 'Add Patternkit block' 55 | options: 56 | _admin_route: TRUE 57 | requirements: 58 | _permission: 'administer blocks' 59 | 60 | patternkit.api: 61 | path: '/api/patternkit' 62 | defaults: 63 | _controller: '\Drupal\patternkit\Controller\PatternkitController::apiPattern' 64 | _disable_route_normalizer: 'TRUE' 65 | requirements: 66 | _access: 'TRUE' 67 | 68 | patternkit.settings: 69 | path: '/admin/config/user-interface/patternkit' 70 | defaults: 71 | _form: '\Drupal\patternkit\Form\PatternkitSettingsForm' 72 | _title: 'Patternkit settings' 73 | _description: 'Configure Patternkit Pattern Library Support.' 74 | options: 75 | _admin_route: TRUE 76 | requirements: 77 | _permission: 'access administration pages' 78 | 79 | patternkit.settings.json_settings: 80 | path: '/admin/config/user-interface/patternkit/json' 81 | defaults: 82 | _form: '\Drupal\patternkit\Form\PatternLibraryJSONForm' 83 | _title: 'Patternkit JSON Library settings' 84 | _description: 'Configure Patternkit JSON Pattern Library Support.' 85 | options: 86 | _admin_route: TRUE 87 | requirements: 88 | _permission: 'access administration pages' 89 | -------------------------------------------------------------------------------- /patternkit.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | logger.channel.patternkit: 3 | parent: logger.channel_base 4 | arguments: ['patternkit'] 5 | path_processor.patterns: 6 | class: Drupal\patternkit\PathProcessor\PathProcessorPatterns 7 | tags: 8 | - { name: path_processor_inbound, priority: 200 } 9 | patternkit.asset.library: 10 | class: Drupal\patternkit\Asset\Library 11 | arguments: ['@cache.discovery', '@config.factory', '@lock', '@patternkit.pattern.discovery', '@patternkit.library.namespace_resolver'] 12 | deprecated: 'The "%service_id%" service is deprecated. Use "patternkit.library.namespace_resolver" or "patternkit.pattern.discovery" instead.' 13 | patternkit.asset.library.parser.base: 14 | abstract: true 15 | arguments: [ '@serialization.json', '%app.root%', '@module_handler', '@theme.manager', '@stream_wrapper_manager', '@library.libraries_directory_file_finder', '@extension.path.resolver' ] 16 | patternkit.asset.library.parser.file: 17 | parent: patternkit.asset.library.parser.base 18 | class: Drupal\patternkit\Asset\PatternLibraryParser\FilePatternLibraryParser 19 | patternkit.asset.library.parser.json: 20 | parent: patternkit.asset.library.parser.base 21 | class: Drupal\patternkit\Asset\PatternLibraryParser\JSONPatternLibraryParser 22 | patternkit.asset.library.parser.twig: 23 | parent: patternkit.asset.library.parser.base 24 | class: Drupal\patternkit\Asset\PatternLibraryParser\TwigPatternLibraryParser 25 | patternkit.library.namespace_resolver: 26 | class: Drupal\patternkit\Asset\LibraryNamespaceResolver 27 | arguments: [ '@service_container', '@library.discovery', '@cache.discovery', '@lock' ] 28 | tags: 29 | - { name: needs_destruction } 30 | patternkit.pattern.discovery: 31 | class: Drupal\patternkit\Asset\PatternDiscovery 32 | arguments: [ '@patternkit.pattern.discovery.collector', '@patternkit.library.namespace_resolver'] 33 | patternkit.pattern.discovery.collector: 34 | class: Drupal\patternkit\Asset\PatternDiscoveryCollector 35 | arguments: [ '@cache.discovery', '@lock', '@patternkit.library.namespace_resolver', '@plugin.manager.library.pattern', '@logger.channel.patternkit'] 36 | tags: 37 | - { name: needs_destruction } 38 | plugin.manager.library.pattern: 39 | class: Drupal\patternkit\PatternLibraryPluginManager 40 | arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler'] 41 | stream_wrapper.library: 42 | class: Drupal\patternkit\StreamWrapper\LibraryStream 43 | tags: 44 | - { name: stream_wrapper, scheme: library } 45 | stream_wrapper.patternkit: 46 | class: Drupal\patternkit\StreamWrapper\PatternkitStream 47 | tags: 48 | - { name: stream_wrapper, scheme: patternkit } 49 | twig.loader.patternlibrary: 50 | class: Drupal\patternkit\Loader\PatternLibraryLoader 51 | # We use '.' instead of '@app.root' as the path for non-namespaced template 52 | # files so that they match the relative paths of templates loaded via the 53 | # theme registry or via Twig namespaces. 54 | arguments: ['.', '@logger.channel.patternkit', '@patternkit.library.namespace_resolver'] 55 | tags: 56 | - { name: twig.loader, priority: 100 } 57 | patternkit.translate_layout_builder_block: 58 | class: Drupal\patternkit\Routing\PatternkitTranslateBlockFormRouteSubscriber 59 | tags: 60 | - { name: event_subscriber } 61 | plugin.manager.pattern_field_processor: 62 | class: Drupal\patternkit\PatternFieldProcessorPluginManager 63 | arguments: 64 | - '@container.namespaces' 65 | - '@cache.discovery' 66 | - '@module_handler' 67 | - '@serialization.json' 68 | - '@patternkit.schema.schema_walker_factory' 69 | - '@logger.channel.patternkit' 70 | patternkit.schema.schema_factory: 71 | class: Drupal\patternkit\Schema\SchemaFactory 72 | arguments: 73 | - '@patternkit.schema.ref_provider' 74 | patternkit.schema.ref_provider: 75 | class: Drupal\patternkit\Schema\PatternkitRefProvider 76 | arguments: 77 | - '@patternkit.pattern.discovery' 78 | - '@patternkit.library.namespace_resolver' 79 | public: false 80 | patternkit.schema.schema_walker_factory: 81 | class: Drupal\patternkit\Schema\SchemaWalkerFactory 82 | arguments: 83 | - '@patternkit.schema.schema_factory' 84 | public: false 85 | -------------------------------------------------------------------------------- /src/AJAX/PatternkitEditorUpdateCommand.php: -------------------------------------------------------------------------------- 1 | selector = $selector; 45 | $this->value = $value; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function render(): array { 52 | return [ 53 | 'command' => 'patternkitEditorUpdate', 54 | 'selector' => $this->selector, 55 | 'value' => $this->value, 56 | ]; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/Annotation/PatternFieldProcessor.php: -------------------------------------------------------------------------------- 1 | definition); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Asset/LibraryInterface.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 46 | $this->resolver = $resolver; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function getPatternsByNamespace(string $namespace): array { 53 | assert($namespace[0] === '@', 'The pattern namespace is expected to begin with an "@" symbol.'); 54 | 55 | if (!isset($this->patternDefinitions[$namespace])) { 56 | $this->patternDefinitions[$namespace] = $this->collector->get($namespace); 57 | } 58 | 59 | return $this->patternDefinitions[$namespace]; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getPatternDefinition(string $pattern): ?array { 66 | $namespace = $this->getNamespaceFromPatternIdentifier($pattern); 67 | 68 | $patterns = $this->getPatternsByNamespace($namespace); 69 | return $patterns[$pattern] ?? NULL; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getPatternDefinitions(): array { 76 | $libraries = $this->resolver->getLibraryDefinitions(); 77 | 78 | foreach ($libraries as $library => $definition) { 79 | // Skip this library if it doesn't define any patterns. 80 | if (!isset($definition['patterns'])) { 81 | continue; 82 | } 83 | 84 | $this->getPatternsByNamespace($library); 85 | } 86 | 87 | return $this->patternDefinitions; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function clearCachedDefinitions(): void { 94 | $this->patternDefinitions = []; 95 | $this->collector->clear(); 96 | } 97 | 98 | /** 99 | * Get the namespace from a fully-namespaced pattern identifier. 100 | * 101 | * @param string $pattern 102 | * The fully-namespaced pattern identifier to parse. 103 | * 104 | * @return string 105 | * The namespace portion of the pattern identifier. 106 | */ 107 | protected function getNamespaceFromPatternIdentifier(string $pattern): string { 108 | return strstr($pattern, '/', TRUE); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/Asset/PatternDiscoveryInterface.php: -------------------------------------------------------------------------------- 1 | serializer::decode($assets['schema']); 31 | $schema = static::schemaDereference( 32 | $schema, 33 | $pattern 34 | ); 35 | $assets['schema'] = $this->serializer::encode($schema); 36 | 37 | return $assets; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function parsePatternLibraryInfo(PatternLibrary $library, string $path): array { 44 | if (!file_exists($path)) { 45 | throw new InvalidLibraryFileException("Path $path does not exist."); 46 | } 47 | $metadata = []; 48 | $info = $library->getPatternInfo(); 49 | if ($path[0] === '/') { 50 | // Trim the root path plus the trailing slash. 51 | $relative_path = substr($path, strlen($this->root . '/')); 52 | } 53 | else { 54 | $relative_path = $path; 55 | } 56 | $pattern_info = $info[$relative_path]; 57 | foreach (self::discoverComponents($path, ['json']) as $name => $data) { 58 | if (empty($data['json']) || !file_exists($data['json'])) { 59 | continue; 60 | } 61 | $pattern_path = trim(substr($data['json'], strlen($path), -strlen('.json')), '/\\'); 62 | 63 | $library_defaults = [ 64 | '$schema' => 'https://json-schema.org/schema#', 65 | 'assets' => ['schema' => $data['json']], 66 | 'category' => $pattern_info['category'] ?? 'default', 67 | 'title' => $name, 68 | 'type' => 'object', 69 | 'format' => 'grid', 70 | 'library' => $library->id(), 71 | 'libraryPluginId' => $pattern_info['plugin'], 72 | 'license' => $library->license ?? [], 73 | 'name' => $name, 74 | 'path' => $pattern_path, 75 | 'properties' => (object) [], 76 | 'required' => [], 77 | 'version' => $library->version ?? '', 78 | ]; 79 | if (!empty($data['json']) && $file_contents = file_get_contents($data['json'])) { 80 | $category_guess = $library->category ?? strstr($pattern_path, DIRECTORY_SEPARATOR, TRUE); 81 | $library_defaults['category'] = $category_guess ?? $library_defaults['category']; 82 | $library_defaults['assets']['json'] = $data['json']; 83 | $pattern = $this->createPattern($name, (array) $this->serializer::decode($file_contents) + $library_defaults); 84 | } 85 | else { 86 | // Create the pattern from defaults. 87 | // @todo Have this cleverly infer defaults from the template. 88 | $pattern = $this->createPattern($name, $library_defaults); 89 | } 90 | $pattern->filename = trim(substr($data['json'], strlen($path)), '/\\'); 91 | // @todo Set $pattern->url to the actual URL of the pattern. 92 | // @todo Add default of library version fallback to extension version. 93 | $pattern->version ??= 'VERSION'; 94 | 95 | $metadata[$pattern->getPath()] = $pattern->toArray(); 96 | } 97 | 98 | return $metadata; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Asset/PatternLibraryParser/WebcomponentPatternLibraryParser.php: -------------------------------------------------------------------------------- 1 | get( 39 | $url, 40 | [ 41 | 'headers' => ['Content-Type' => 'application/json'], 42 | 'jsondata' => $config->rawJSON, 43 | // 'timeout' => 10,. 44 | 'method' => 'POST', 45 | ] 46 | ); 47 | 48 | // @todo Request failure handling. 49 | // Create the stub object. 50 | $pk_obj = (object) [ 51 | 'PatternkitPattern' => $subtype, 52 | 'attachments' => [], 53 | 'body' => 'fragment.html', 54 | ]; 55 | 56 | $dir = "public://patternkit/$subtype/{$config->instance_id}"; 57 | if (!\Drupal::service('file_system')->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY)) { 58 | \Drupal::service('messenger')->addMessage( 59 | t( 60 | 'Unable to create folder or save metadata/assets for plugin @plugin', 61 | ['@plugin' => $subtype] 62 | )); 63 | \Drupal::logger('patternkit')->error( 64 | 'Unable to create folder or save metadata/assets for plugin @plugin', 65 | ['@plugin' => $subtype]); 66 | } 67 | 68 | // Fetch the body html artifact. 69 | $save_result = \Drupal::service('file_system')->saveData( 70 | $request->getBody()->getContents(), 71 | "$dir/body.html", 72 | FileSystemInterface::EXISTS_REPLACE 73 | ); 74 | 75 | // Convert stream wrapper to relative path, if appropriate. 76 | $scheme = StreamWrapperManagerInterface::getScheme($save_result); 77 | if ($scheme && \Drupal::service('stream_wrapper_manager')->isValidScheme($scheme)) { 78 | $wrapper = \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme); 79 | $save_result = $wrapper->getDirectoryPath() . "/" . \Drupal::service('stream_wrapper_manager')::getTarget( 80 | $save_result); 81 | } 82 | 83 | $pk_obj->body = $save_result; 84 | 85 | $pk_obj->attachments['js']['https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.24/webcomponents.min.js'] = [ 86 | 'type' => 'external', 87 | 'scope' => 'header', 88 | 'group' => JS_DEFAULT, 89 | 'weight' => 0, 90 | ]; 91 | 92 | // Add the header link rel import. 93 | $pk_obj->attachments['drupal_add_html_head_link'][] = [ 94 | [ 95 | 'rel' => 'import', 96 | 'href' => $pk_obj->body, 97 | ], 98 | ]; 99 | 100 | if (!$save_result) { 101 | \Drupal::service('messenger')->addMessage( 102 | t('Unable to save metadata/assets for plugin @plugin', 103 | ['@plugin' => $subtype])); 104 | \Drupal::logger('patternkit')->error( 105 | 'Unable to save metadata/assets for plugin @plugin', 106 | ['@plugin' => $subtype]); 107 | // @todo Handle the failure gracefully. 108 | } 109 | 110 | return Pattern::create((array) $pk_obj); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function parsePatternLibraryInfo(PatternLibrary $library, $path): array { 117 | // @todo Implement parsePatternLibraryInfo() method. 118 | return []; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Element/PatternError.php: -------------------------------------------------------------------------------- 1 | 'pattern_error', 24 | * '#pattern' => Pattern, 25 | * '#config' => [ 26 | * 'text' => '[node:title]', 27 | * 'formatted_text' => '

My formatted text

', 28 | * ], 29 | * '#context' => [ 30 | * 'node' => $node, 31 | * ], 32 | * '#exception' => Exception, 33 | * ]; 34 | * @endcode 35 | * 36 | * @RenderElement("pattern_error") 37 | */ 38 | class PatternError extends RenderElement { 39 | 40 | /** 41 | * The permission to check for including debug output in error displays. 42 | */ 43 | const DEBUG_PERMISSION = 'access devel information'; 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getInfo(): array { 49 | return [ 50 | '#pre_render' => [ 51 | [$this, 'preRenderPatternErrorElement'], 52 | [$this, 'preRenderDebugOutput'], 53 | ], 54 | '#pattern' => NULL, 55 | '#config' => [], 56 | '#context' => [], 57 | '#exception' => NULL, 58 | ]; 59 | } 60 | 61 | /** 62 | * Pattern error element pre render callback to handle basic failure display. 63 | * 64 | * @param array $element 65 | * An associative array containing the properties of the pattern element. 66 | * 67 | * @return array 68 | * The modified element. 69 | */ 70 | public function preRenderPatternErrorElement(array $element): array { 71 | /** @var \Drupal\patternkit\Entity\Pattern $pattern */ 72 | $pattern = $element['#pattern']; 73 | 74 | // Return an error message to display in place of the rendered pattern. 75 | $message = $this->t('Failed to render pattern %pattern (%pattern_id).', [ 76 | '%pattern' => $pattern->getName(), 77 | '%pattern_id' => $pattern->getAssetId(), 78 | ]); 79 | $element['message'] = [ 80 | '#markup' => $message, 81 | ]; 82 | 83 | return $element; 84 | } 85 | 86 | /** 87 | * Secondary pre render callback to prepare debug information if applicable. 88 | * 89 | * @param array $element 90 | * An associative array containing the properties of the pattern element. 91 | * 92 | * @return array 93 | * The modified element. 94 | */ 95 | public function preRenderDebugOutput(array $element): array { 96 | // Skip altogether if the user doesn't have access to dev output. 97 | if (!$this->shouldDisplayDebugOutput()) { 98 | return $element; 99 | } 100 | 101 | if (isset($element['#exception'])) { 102 | /** @var \Exception $exception */ 103 | $exception = $element['#exception']; 104 | 105 | // Collect all debug information in a collapsed container to avoid 106 | // overwhelming the user, especially in the case of multiple failures on 107 | // a single page. 108 | $element['debug'] = [ 109 | '#type' => 'details', 110 | '#title' => $this->t('Debug output'), 111 | '#open' => FALSE, 112 | ]; 113 | 114 | // Expose the exception message in a formatted block for easier 115 | // parsing by developers. 116 | $element['debug']['message'] = [ 117 | '#markup' => '
' . $exception->getMessage() . '
', 118 | ]; 119 | } 120 | 121 | return $element; 122 | } 123 | 124 | /** 125 | * Test if debug output should be displayed. 126 | * 127 | * @return bool 128 | * TRUE if debug output should be displayed. FALSE otherwise. 129 | */ 130 | protected function shouldDisplayDebugOutput(): bool { 131 | return \Drupal::currentUser()->hasPermission(static::DEBUG_PERMISSION); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Entity/PatternInterface.php: -------------------------------------------------------------------------------- 1 | addDefaults([ 22 | '_controller' => '\Drupal\patternkit\Controller\PatternkitController::patternView', 23 | '_title_callback' => '\Drupal\patternkit\Controller\PatternkitController::patternTitle', 24 | ]) 25 | ->setRequirement('patternkit_pattern', '\d+') 26 | ->setRequirement('_entity_access', 'patternkit_pattern.view'); 27 | $route_collection->add('entity.patternkit_pattern.canonical', $route); 28 | 29 | return $route_collection; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/SchemaException.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Get the schema being processed at the time of the exception. 35 | * 36 | * @return \Swaggest\JsonSchema\SchemaContract|null 37 | * The schema being processed at the time of the exception or null if it 38 | * was not set. 39 | */ 40 | public function getSchema(): ?SchemaContract { 41 | return $this->schema; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Exception/SchemaReferenceException.php: -------------------------------------------------------------------------------- 1 | entity; 28 | 29 | $form = parent::form($form, $form_state); 30 | 31 | if ($this->operation == 'edit') { 32 | $form['#title'] = $this->t('Edit Patternkit block %label', ['%label' => $block->label()]); 33 | } 34 | // Override the default CSS class name, since the user-defined custom block 35 | // type name in 'TYPE-block-form' potentially clashes with third-party class 36 | // names. 37 | $form['#attributes']['class'][0] = 'block-' . Html::getClass($block->bundle()) . '-form'; 38 | 39 | return $form; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function save(array $form, FormStateInterface $form_state): void { 46 | $block = $this->entity; 47 | 48 | $insert = $block->isNew(); 49 | $block->setReusable(); 50 | $block->save(); 51 | $context = ['@type' => $block->bundle(), '%info' => $block->label()]; 52 | $logger = $this->logger('patternkit'); 53 | $t_args = ['@type' => $block->bundle(), '%info' => $block->label()]; 54 | 55 | if ($insert) { 56 | $logger->notice('@type: added %info.', $context); 57 | $this->messenger()->addStatus($this->t('@type %info has been created.', $t_args)); 58 | } 59 | else { 60 | $logger->notice('@type: updated %info.', $context); 61 | $this->messenger()->addStatus($this->t('@type %info has been updated.', $t_args)); 62 | } 63 | 64 | if ($block->id()) { 65 | $form_state->setValue('id', $block->id()); 66 | $form_state->set('id', $block->id()); 67 | if ($insert) { 68 | if (!$theme = $block->getTheme()) { 69 | $theme = $this->config('system.theme')->get('default'); 70 | } 71 | if (!$pattern = $block->getPattern()) { 72 | $block->setPattern($form_state->getValue('pattern')); 73 | } 74 | $form_state->setRedirect( 75 | 'block.admin_add', 76 | [ 77 | 'plugin_id' => 'patternkit_block:' . $block->uuid(), 78 | 'theme' => $theme, 79 | ] 80 | ); 81 | } 82 | else { 83 | $form_state->setRedirectUrl($block->toUrl('collection')); 84 | } 85 | } 86 | else { 87 | // In the unlikely case something went wrong on save, the block will be 88 | // rebuilt and block form redisplayed. 89 | $this->messenger()->addError($this->t('The block could not be saved.')); 90 | $form_state->setRebuild(); 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/FormElement/PatternkitJson.php: -------------------------------------------------------------------------------- 1 | getId(); 25 | 26 | $container = \Drupal::getContainer(); 27 | $patternBlock = PatternkitBlock::create($container, $configuration, 'patternkit_block', []); 28 | // Form state not used in this method, but is required for the block form. 29 | $form_state = new FormState(); 30 | 31 | $form = $patternBlock->blockForm($form, $form_state); 32 | $form['langcode'] = [ 33 | '#type' => 'hidden', 34 | '#default_value' => $translation_language->getId(), 35 | ]; 36 | $form['patternkit_block_rid'] = [ 37 | '#type' => 'hidden', 38 | '#default_value' => $configuration['patternkit_block_rid'], 39 | ]; 40 | 41 | $form['patternkit_block_id'] = [ 42 | '#type' => 'hidden', 43 | '#default_value' => $configuration['patternkit_block_id'], 44 | ]; 45 | 46 | $form['instance_config']['#default_value'] = $translation_config['instance_config']; 47 | 48 | return $form; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Loader/PatternLibraryLoader.php: -------------------------------------------------------------------------------- 1 | getLibraryDefinitions(); 34 | } 35 | catch (\Exception $exception) { 36 | $logger->error('Error loading pattern libraries: @message', ['@message' => $exception->getMessage()]); 37 | } 38 | foreach ($libraries as $namespace => $pattern_library) { 39 | if (isset($pattern_library['patterns'])) { 40 | foreach ($pattern_library['patterns'] as $info) { 41 | if (!isset($info['data'])) { 42 | continue; 43 | } 44 | 45 | // Trim the leading symbol for registered namespaces since they are 46 | // trimmed before lookup when loading templates. 47 | $this->addPath($info['data'], ltrim($namespace, '@')); 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Adds a path where templates are stored. 55 | * 56 | * @param string $path 57 | * A path where to look for templates. 58 | * @param string $namespace 59 | * (optional) A path name. 60 | */ 61 | public function addPath($path, $namespace = self::MAIN_NAMESPACE): void { 62 | // Invalidate the cache. 63 | $this->cache = []; 64 | $this->paths[$namespace][] = rtrim($path, '/\\'); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/PathProcessor/PathProcessorPatterns.php: -------------------------------------------------------------------------------- 1 | query->has('pattern')) { 22 | 23 | $pattern_path = preg_replace('|^\/api\/patternkit\/|', '', $path); 24 | $request->query->set('pattern', $pattern_path); 25 | return '/api/patternkit'; 26 | } 27 | return $path; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/PatternEditorConfig.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (property_exists($this, (string) $property)) { 19 | $this->{$property} = $value; 20 | unset($config->{$property}); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * JSON-style field values used in provisioning the editor. 27 | * 28 | * @var object 29 | */ 30 | public $fields; 31 | 32 | /** 33 | * Application hostname. 34 | * 35 | * @var string 36 | */ 37 | public $hostname; 38 | 39 | /** 40 | * The URL to the icon stylesheet. 41 | * 42 | * @var string 43 | */ 44 | public $iconStylesheet; 45 | 46 | // phpcs:disable Drupal.NamingConventions.ValidVariableName.LowerCamelName 47 | /** 48 | * The config instance id. 49 | * 50 | * Uses underscore_case for Drupal compatibility. 51 | * 52 | * @var string 53 | */ 54 | public $instance_id; 55 | 56 | // phpcs:enable Drupal.NamingConventions.ValidVariableName.LowerCamelName 57 | /** 58 | * Optional lz-compressed serialized version of the configuration. 59 | * 60 | * @var string 61 | */ 62 | public $lzstring; 63 | 64 | /** 65 | * Optional pattern to use for configuring the editor. 66 | * 67 | * @var \Drupal\patternkit\Entity\Pattern 68 | */ 69 | public $pkdata; 70 | 71 | // phpcs:disable Drupal.NamingConventions.ValidVariableName.LowerCamelName 72 | /** 73 | * The presentation style for the pattern. 74 | * 75 | * Uses underscore_case for Drupal compatibility. 76 | * 77 | * @var string 78 | * Typically the following: 79 | * - html 80 | */ 81 | public $presentation_style; 82 | 83 | // phpcs:enable Drupal.NamingConventions.ValidVariableName.LowerCamelName 84 | /** 85 | * The raw Editor configuration JSON. 86 | * 87 | * @var string 88 | */ 89 | public $rawJSON; 90 | 91 | /** 92 | * The active theme. 93 | * 94 | * @var string 95 | */ 96 | public $theme; 97 | 98 | /** 99 | * The theme stylesheet. 100 | * 101 | * @var string 102 | */ 103 | public $themeStylesheet; 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/PatternLibraryPluginInterface.php: -------------------------------------------------------------------------------- 1 | t('Edit @title', ['@title' => $entity->label()]); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Plugin/Field/FieldType/SerializedData.php: -------------------------------------------------------------------------------- 1 | setLabel(new TranslatableMarkup('Serialized data')) 28 | ->setRequired(TRUE); 29 | 30 | return $properties; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public static function schema(FieldStorageDefinitionInterface $field_definition): array { 37 | return [ 38 | 'columns' => [ 39 | 'value' => [ 40 | 'type' => 'blob', 41 | 'size' => 'big', 42 | 'serialize' => TRUE, 43 | ], 44 | ], 45 | ]; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Plugin/Menu/LocalAction/PatternkitAddLocalAction.php: -------------------------------------------------------------------------------- 1 | getParameter('theme')) { 21 | $options['query']['theme'] = $theme; 22 | } 23 | // Adds a destination on custom block listing. 24 | if ($route_match->getRouteName() === 'entity.patternkit.collection') { 25 | $options['query']['destination'] = Url::fromRoute('')->toString(); 26 | } 27 | return $options; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Plugin/PatternFieldProcessor/PatternFieldProcessorBase.php: -------------------------------------------------------------------------------- 1 | pluginDefinition['label']; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Plugin/PatternFieldProcessor/PatternFieldProcessorInterface.php: -------------------------------------------------------------------------------- 1 | token = $token; 54 | $this->twig = $twig; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { 61 | /** @var \Drupal\Core\Utility\Token $token */ 62 | $token = $container->get('token'); 63 | /** @var \Drupal\Core\Template\TwigEnvironment $twig */ 64 | $twig = $container->get('twig'); 65 | return new static( 66 | $configuration, 67 | $plugin_id, 68 | $plugin_definition, 69 | $token, 70 | $twig, 71 | ); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function applies(SchemaContract $propertySchema, $propertyValue = NULL): bool { 78 | return property_exists($propertySchema, 'type') 79 | && $propertySchema->type == 'string'; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function apply(SchemaContract $propertySchema, $value, $context, BubbleableMetadata $bubbleableMetadata): string { 86 | $token_groups = $this->token->scan($value); 87 | $template = $value; 88 | $template_context = []; 89 | foreach ($token_groups as $group => $tokens) { 90 | $tokenized = $this->token->generate( 91 | $group, 92 | $tokens, 93 | $context, 94 | [], 95 | $bubbleableMetadata 96 | ); 97 | foreach ($tokens as $token) { 98 | // Abort token replacement if we don't have a replacement value. 99 | if (!isset($tokenized[$token])) { 100 | continue; 101 | } 102 | 103 | $placeholder = preg_replace("/[^a-z]/", '', $token); 104 | $template_context[$placeholder] = $tokenized[$token]; 105 | // If the user is not using Twig templating, 106 | // wrap with a Twig write so we can process it. 107 | $token_pos = strpos($template, (string) $token); 108 | $template_use_twig = strpos($template, '{{') < $token_pos 109 | && strpos($template, '}}', $token_pos + strlen($token)) !== FALSE; 110 | if (!$template_use_twig) { 111 | $placeholder = '{{' . $placeholder . '}}'; 112 | } 113 | $template = str_replace($token, $placeholder, $template); 114 | } 115 | } 116 | return (string) $this->twig->renderInline($template, $template_context); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/Plugin/PatternFieldProcessor/WysiwygFieldProcessor.php: -------------------------------------------------------------------------------- 1 | patternkitSettings = $patternkitSettings; 54 | $this->renderer = $renderer; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { 61 | /** @var \Drupal\Core\Config\ConfigFactoryInterface $configFactory */ 62 | $configFactory = $container->get('config.factory'); 63 | $settings = $configFactory->get('patternkit.settings'); 64 | /** @var \Drupal\Core\Render\RendererInterface $renderer */ 65 | $renderer = $container->get('renderer'); 66 | return new static( 67 | $configuration, 68 | $plugin_id, 69 | $plugin_definition, 70 | $settings, 71 | $renderer, 72 | ); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | * 78 | * Expect a global text profile to be configured, and a property schema like 79 | * the following: 80 | * 81 | * @code 82 | * { 83 | * "type" => "string", 84 | * "format" => "html", 85 | * "options" => { 86 | * "wysiwyg" => true, 87 | * } 88 | * } 89 | * @endcode 90 | */ 91 | public function applies(SchemaContract $propertySchema, $propertyValue = NULL): bool { 92 | $textProfile = $this->getTextProfile(); 93 | return is_string($textProfile) && $textProfile !== '' 94 | && property_exists($propertySchema, 'type') && $propertySchema->type == 'string' 95 | && property_exists($propertySchema, 'format') && $propertySchema->format == 'html' 96 | && isset($propertySchema->options) && property_exists($propertySchema->options, 'wysiwyg') 97 | && $propertySchema->options->wysiwyg == TRUE; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function apply(SchemaContract $propertySchema, $value, array $context, BubbleableMetadata $bubbleableMetadata): string { 104 | $element = [ 105 | '#type' => 'processed_text', 106 | '#text' => $value, 107 | '#format' => $this->getTextProfile(), 108 | ]; 109 | 110 | return (string) $this->renderer->render($element); 111 | } 112 | 113 | /** 114 | * Get the globally configured WYSIWYG text editor profile. 115 | * 116 | * @return string|null 117 | * The machine name of the text profile configured in Patternkit settings. 118 | */ 119 | public function getTextProfile(): ?string { 120 | return $this->patternkitSettings->get('patternkit_json_editor_ckeditor_toolbar'); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/Plugin/PatternLibrary/PatternLibraryFile.php: -------------------------------------------------------------------------------- 1 | getParameter('app.root'); 51 | /** @var \Drupal\patternkit\Asset\PatternLibraryParserInterface $file_parser */ 52 | $file_parser = $container->get('patternkit.asset.library.parser.file'); 53 | /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */ 54 | $config_factory = $container->get('config.factory'); 55 | return new static($root, $file_parser, $config_factory, $configuration, $plugin_id, $plugin_definition); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getEditor(PatternInterface $pattern = NULL, ?PatternEditorConfig $config = NULL): string { 62 | // @todo Implement getEditor() method. 63 | return ''; 64 | } 65 | 66 | /** 67 | * Overrides the JSON Library render method. 68 | * 69 | * {@inheritdoc} 70 | * 71 | * @throws \Throwable 72 | * 73 | * @todo Return render arrays for File only. 74 | */ 75 | public function render(array $assets): array { 76 | $elements = []; 77 | foreach ($assets as $pattern) { 78 | if (empty($pattern->filename)) { 79 | return []; 80 | } 81 | // @todo Allow filename to cache the template contents based on settings. 82 | $template = $pattern->filename; 83 | // Add the namespace, if provided. 84 | if (!empty($pattern->url)) { 85 | $template = '@' . $pattern->url . '#/' . $template; 86 | } 87 | $namespace = ''; 88 | $file = $template; 89 | // If a namespace is provided, break it up. 90 | if (strpos($template, '@') === 0) { 91 | [$namespace, $file] = explode('#', $template); 92 | } 93 | $bare = basename($file); 94 | /** @var \Drupal\Core\Template\TwigEnvironment $twig */ 95 | $twig = \Drupal::service('twig'); 96 | $template = $twig->load("$namespace/$pattern->filename"); 97 | $elements[] = $template->render($pattern->config ?? []); 98 | } 99 | return $elements; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/Plugin/PatternLibrary/PatternLibraryREST.php: -------------------------------------------------------------------------------- 1 | state = $state; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getEditor(PatternInterface $pattern = NULL, 58 | PatternEditorConfig $config = NULL): void { 59 | // @todo Implement getEditor() method. 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getMetadata(Extension $extension, PatternLibrary $library, string $path): array { 66 | // @todo Implement getMetadata() method. 67 | return []; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | * 73 | * This class evaluates the aggregation enabled/disabled condition on a group 74 | * by group basis by testing whether an aggregate file has been made for the 75 | * group rather than by testing the site-wide aggregation setting. This allows 76 | * this class to work correctly even if modules have implemented custom 77 | * logic for grouping and aggregating files. 78 | * 79 | * @throws \Exception 80 | */ 81 | public function render(array $assets): array { 82 | $elements = []; 83 | /** @var \Drupal\patternkit\Entity\PatternInterface $pattern */ 84 | foreach ($assets as $pattern) { 85 | $config = $pattern->config ?? []; 86 | if (empty($config['presentation_style']) || empty($config['instance_id'])) { 87 | return []; 88 | } 89 | $pattern_name = $pattern->label(); 90 | if ($config['presentation_style'] === 'webcomponent') { 91 | $elements[] = "<$pattern_name-pattern>"; 92 | } 93 | else { 94 | $filename = "public://patternkit/$pattern_name/{$config['instance_id']}/body.html"; 95 | $elements[] = file_get_contents($filename); 96 | } 97 | } 98 | return $elements; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/Plugin/PatternLibrary/PatternLibraryTwig.php: -------------------------------------------------------------------------------- 1 | parser = $container->get('patternkit.asset.library.parser.twig'); 38 | 39 | return $instance; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function fetchAssets(PatternInterface $pattern, ?PatternEditorConfig $config = NULL): array { 46 | return $this->parser->fetchPatternAssets($pattern, $config); 47 | } 48 | 49 | /** 50 | * Overrides the JSON Library render method. 51 | * 52 | * {@inheritdoc} 53 | * 54 | * @throws \Throwable 55 | * 56 | * @todo Return render arrays for Twig only. 57 | */ 58 | public function render(array $assets): array { 59 | $elements = []; 60 | /** @var \Drupal\patternkit\Entity\Pattern $pattern */ 61 | foreach ($assets as $pattern) { 62 | $template = $pattern->getTemplate(); 63 | if (empty($template)) { 64 | return []; 65 | } 66 | $pattern->config ??= []; 67 | $output = [ 68 | '#type' => 'inline_template', 69 | '#template' => $template, 70 | '#context' => $pattern->config, 71 | ]; 72 | if ($this->getTwigEnvironment()->isDebug()) { 73 | $hash = $pattern->getHash(); 74 | $asset_id = $pattern->getAssetId(); 75 | $path = trim($pattern->getAssets()['twig'] ?: $this->t('Could not resolve file path.'), DRUPAL_ROOT); 76 | try { 77 | $version_value = $pattern->get('version')->getValue(); 78 | $version = array_pop($version_value)['value']; 79 | } 80 | catch (\Exception $exception) { 81 | $version = $this->t('No version information available.'); 82 | } 83 | $output['#template'] = "\n\n" 84 | . "\n' 85 | . "\n' 86 | . "\n' 87 | . "\n\n\n" 88 | . $output['#template'] 89 | .= "\n\n\n"; 90 | } 91 | $elements[] = $output; 92 | } 93 | return $elements; 94 | } 95 | 96 | /** 97 | * Get the lazy-loaded twig environment service. 98 | * 99 | * Since this plugin is loaded from the patternkit twig loader 100 | * ('@twig.loader.patternlibrary'), a circular dependency on the twig 101 | * environment is created. To resolve this, the twig enviornment is only 102 | * needed at render time for this plugin, so it is lazy loaded from the 103 | * container at that time. 104 | * 105 | * @return \Drupal\Core\Template\TwigEnvironment 106 | * The twig environment service. 107 | * 108 | * @see \Drupal\patternkit\Loader\PatternLibraryLoader 109 | */ 110 | public function getTwigEnvironment(): TwigEnvironment { 111 | if (!$this->twig) { 112 | $this->twig = \Drupal::service('twig'); 113 | } 114 | 115 | return $this->twig; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Routing/PatternkitTranslateBlockFormRouteSubscriber.php: -------------------------------------------------------------------------------- 1 | get('layout_builder.translate_block')) { 18 | $route->setDefault('_form', '\Drupal\patternkit\Form\PatternkitTranslateBlockForm'); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Schema/DataPreProcessor/ObjectCoercionDataPreProcessor.php: -------------------------------------------------------------------------------- 1 | type == 'object' && is_array($data)) { 18 | $data = (object) $data; 19 | } 20 | 21 | return $data; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Schema/SchemaWalker.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 37 | 38 | $schemaIterator = new SchemaIterator($schema, $values, $flags); 39 | parent::__construct($schemaIterator, $mode, $flags); 40 | } 41 | 42 | /** 43 | * Get the top-level schema being traversed. 44 | * 45 | * @return \Swaggest\JsonSchema\SchemaContract 46 | * The schema definition being traversed. 47 | */ 48 | public function getSchema(): SchemaContract { 49 | return $this->schema; 50 | } 51 | 52 | /** 53 | * Get the schema for the current property in traversal. 54 | * 55 | * @return \Swaggest\JsonSchema\SchemaContract|null 56 | * The loaded schema for the current property or NULL if one was unable to 57 | * be identified. 58 | * 59 | * @throws \Drupal\patternkit\Exception\SchemaValidationException 60 | */ 61 | public function getPropertySchema(): ?SchemaContract { 62 | /** @var \Drupal\patternkit\Schema\SchemaIterator $iterator */ 63 | $iterator = $this->getSubIterator(); 64 | 65 | if ($iterator) { 66 | return $iterator->getPropertySchema(); 67 | } 68 | else { 69 | return NULL; 70 | } 71 | } 72 | 73 | /** 74 | * Set the current key to the given value and propagate it up the tree. 75 | * 76 | * @param mixed $value 77 | * The value to be set at the current iteration key. 78 | */ 79 | public function setCurrentValue($value): void { 80 | // Get the current depth and traverse back up the tree, saving the 81 | // modifications at each level along the way. 82 | $currentDepth = $this->getDepth(); 83 | for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) { 84 | // Get the current level iterator. 85 | /** @var \Drupal\patternkit\Schema\SchemaIterator $subIterator */ 86 | $subIterator = $this->getSubIterator($subDepth); 87 | // If we are on the level we want to change, use the replacement ($value), 88 | // otherwise set the key to the parent iterator's value. 89 | $subIterator->offsetSet($subIterator->key(), 90 | ($subDepth === $currentDepth ? $value : $this->getSubIterator(($subDepth + 1))->getArrayCopy())); 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Schema/SchemaWalkerFactory.php: -------------------------------------------------------------------------------- 1 | schemaFactory = $schemaFactory; 31 | } 32 | 33 | /** 34 | * Create a new SchemaWalker instance. 35 | * 36 | * @param \Swaggest\JsonSchema\SchemaContract $schema 37 | * The schema for reference during traversal with values. 38 | * @param array|object $values 39 | * The values to be used to traverse the schema. 40 | * 41 | * @return \Drupal\patternkit\Schema\SchemaWalker 42 | * A configured SchemaWalker instance. 43 | */ 44 | public function createInstance(SchemaContract $schema, $values): SchemaWalker { 45 | return new SchemaWalker($schema, $values); 46 | } 47 | 48 | /** 49 | * Create a new SchemaWalker from a schema's JSON string. 50 | * 51 | * @param string $schema_json 52 | * The string representation of the schema to be used. 53 | * @param array|object $values 54 | * Values to be traversed against the schema. 55 | * 56 | * @return \Drupal\patternkit\Schema\SchemaWalker 57 | * A configured SchemaWalker instance. 58 | * 59 | * @throws \Drupal\patternkit\Exception\SchemaReferenceException 60 | * @throws \Drupal\patternkit\Exception\SchemaValidationException 61 | * @throws \Drupal\patternkit\Exception\SchemaException 62 | */ 63 | public function createFromString(string $schema_json, $values): SchemaWalker { 64 | $schema = $this->schemaFactory->createInstance($schema_json); 65 | 66 | return $this->createInstance($schema, $values); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/StreamWrapper/LibraryStream.php: -------------------------------------------------------------------------------- 1 | uri; 23 | } 24 | 25 | [, $path] = explode('://', $uri, 2); 26 | 27 | // Remove erroneous leading or trailing, forward-slashes and backslashes. 28 | $uri = trim($path, '\/'); 29 | 30 | /** @var \Drupal\Core\Asset\AssetResolverInterface $asset_resolver */ 31 | $asset_resolver = \Drupal::service('asset.resolver'); 32 | [$extension, $library, $type_level] = explode('/', $uri); 33 | // @todo Allow retrieving specific filenames and css levels. 34 | $filename = substr($uri, strlen($extension . $library . $type_level)); 35 | [$type, $level] = array_pad(explode('.', $type_level), 2, NULL); 36 | // Resolve the attached libraries into asset collections. 37 | $assets = new AttachedAssets(); 38 | $assets->setLibraries(["$extension/$library"]); 39 | switch ($type) { 40 | case 'js': 41 | $js_header_footer = $asset_resolver->getJsAssets($assets, TRUE); 42 | $query_assets = array_merge($js_header_footer[0], $js_header_footer[1]); 43 | break; 44 | 45 | case 'css': 46 | $query_assets = $asset_resolver->getCssAssets($assets, TRUE); 47 | break; 48 | 49 | default: 50 | $js = $asset_resolver->getJsAssets($assets, TRUE); 51 | $css = $asset_resolver->getCssAssets($assets, TRUE); 52 | // @todo Create a temporary {libraryname}.json that holds the data. 53 | return ['js' => $js, 'css' => $css]; 54 | } 55 | [, $target] = explode('://', reset($query_assets)['data'], 2); 56 | return $target; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function getName() { 63 | return t('Library files'); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getDescription() { 70 | return t('Library files for usage in processing and rendering.'); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/StreamWrapper/PatternkitStream.php: -------------------------------------------------------------------------------- 1 | getTempDirectory(); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getExternalUrl() { 49 | $path = str_replace('\\', '/', $this->getTarget()); 50 | return Url::fromRoute('system.patternkit', [], [ 51 | 'absolute' => TRUE, 52 | 'query' => ['file' => $path], 53 | ])->toString(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /templates/patternkit-add-list.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * @file 4 | * Default theme implementation to present a list of Patternkit pattern blocks. 5 | * 6 | * Available content variables: 7 | * - types: A collection of all the available custom block types. 8 | * Each block type contains the following: 9 | * - link: A link to add a block of this type. 10 | * - description: A description of this custom block type. 11 | * 12 | * @ingroup themeable 13 | */ 14 | #} 15 | {% spaceless %} 16 |
17 | {% for type in content.types %} 18 |
{{ type.link }}
19 |
{{ type.description }}
20 | {% endfor %} 21 |
22 | {% endspaceless %} 23 | -------------------------------------------------------------------------------- /tests/modules/patternkit_test/lib: -------------------------------------------------------------------------------- 1 | ../../../modules/patternkit_example/lib -------------------------------------------------------------------------------- /tests/modules/patternkit_test/patternkit_test.info.yml: -------------------------------------------------------------------------------- 1 | name: 'Patternkit module tests' 2 | type: module 3 | description: 'Support module for patternkit related testing.' 4 | package: Testing 5 | version: VERSION 6 | -------------------------------------------------------------------------------- /tests/modules/patternkit_test/patternkit_test.libraries.yml: -------------------------------------------------------------------------------- 1 | patternkit_twig: 2 | version: VERSION 3 | css: 4 | theme: 5 | lib/patternkit/dist/patternkit.css: { } 6 | js: 7 | lib/patternkit/dist/patternkit.min.js: { } 8 | patterns: 9 | lib/patternkit/src: {plugin: twig} 10 | 11 | patternkit_json: 12 | version: VERSION 13 | css: 14 | theme: 15 | lib/patternkit/dist/patternkit.css: {} 16 | js: 17 | lib/patternkit/dist/patternkit.min.js: {} 18 | patterns: 19 | lib/patternkit/src: {plugin: json} 20 | -------------------------------------------------------------------------------- /tests/modules/patternkit_test/src/Plugin/PatternFieldProcessor/ExceptionThrowerProcessor.php: -------------------------------------------------------------------------------- 1 | container->get('entity_type.manager'); 53 | $this->patternBlockStorage = $entity_type_manager->getStorage('patternkit_block'); 54 | $this->patternStorage = $entity_type_manager->getStorage('patternkit_pattern'); 55 | } 56 | 57 | /** 58 | * Test that the settings form loads as expected. 59 | */ 60 | public function testPatternkitSettingsForm() { 61 | $account = $this->drupalCreateUser(['access administration pages']); 62 | $this->drupalLogin($account); 63 | 64 | $this->drupalGet('admin/config/user-interface/patternkit'); 65 | 66 | $assert = $this->assertSession(); 67 | 68 | $assert->statusCodeEquals(200); 69 | $assert->pageTextNotContains('Unable to load Patternkit libraries list. Check the logs for more information.'); 70 | $assert->pageTextNotContains('Settings are unavailable when Pattern libraries fail to load to prevent config errors.'); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tests/src/Kernel/Asset/PatternDiscoveryTest.php: -------------------------------------------------------------------------------- 1 | discovery = $this->container->get('patternkit.pattern.discovery'); 39 | } 40 | 41 | /** 42 | * @covers ::getPatternsByNamespace 43 | */ 44 | public function testGetPatternsByNamespace() { 45 | $definitions = $this->discovery->getPatternsByNamespace('@patternkit'); 46 | 47 | $this->assertIsArray($definitions); 48 | $this->assertCount(3, $definitions); 49 | $this->assertArrayHasKey('@patternkit/atoms/example/src/example', $definitions); 50 | $this->assertArrayHasKey('@patternkit/atoms/example_filtered/src/example_filtered', $definitions); 51 | $this->assertArrayHasKey('@patternkit/atoms/example_ref/src/example_ref', $definitions); 52 | } 53 | 54 | /** 55 | * @covers ::getPatternDefinitions 56 | */ 57 | public function testGetPatternDefinitions() { 58 | $definitions = $this->discovery->getPatternDefinitions(); 59 | 60 | $this->assertIsArray($definitions); 61 | 62 | // With these enabled modules, there should only be one namespace defined. 63 | $this->assertCount(1, $definitions); 64 | $this->assertArrayHasKey('@patternkit', $definitions); 65 | 66 | // Within the @patternkit namespace there should be 3 definitions. 67 | $this->assertArrayHasKey('@patternkit/atoms/example/src/example', $definitions['@patternkit']); 68 | $this->assertArrayHasKey('@patternkit/atoms/example_filtered/src/example_filtered', $definitions['@patternkit']); 69 | $this->assertArrayHasKey('@patternkit/atoms/example_ref/src/example_ref', $definitions['@patternkit']); 70 | } 71 | 72 | /** 73 | * @covers ::clearCachedDefinitions 74 | */ 75 | public function testClearCachedDefinitions() { 76 | $cache = $this->container->get('cache.discovery'); 77 | $collector = $this->container->get('patternkit.pattern.discovery.collector'); 78 | 79 | $cacheEntry = $cache->get($collector::PERSISTENT_CACHE_ID); 80 | $this->assertFalse($cacheEntry, 'Expected the pattern discovery cache to be empty initially.'); 81 | 82 | $definitions = $this->discovery->getPatternDefinitions(); 83 | 84 | // Trigger end of request destruction to persist data for testing. 85 | $collector->destruct(); 86 | 87 | $cacheEntry = $cache->get($collector::PERSISTENT_CACHE_ID); 88 | $this->assertIsArray($cacheEntry->data); 89 | $this->assertNotEmpty($cacheEntry->data); 90 | $this->assertArrayHasKey('@patternkit', $cacheEntry->data, 'Expected cache entry to contain "@patternkit" patterns.'); 91 | $this->assertNotEmpty($cacheEntry->data['@patternkit'], 'Expected patterns to be discovered and cached in the "@patternkit" namespace.'); 92 | 93 | $this->discovery->clearCachedDefinitions(); 94 | $cacheEntry = $cache->get($collector::PERSISTENT_CACHE_ID); 95 | $this->assertFalse($cacheEntry, 'Expected pattern cache entry to be empty after clearing definitions.'); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tests/src/Kernel/Asset/PatternLibraryParser/PatternLibraryParserTestBase.php: -------------------------------------------------------------------------------- 1 | patternDiscovery = $this->container->get('patternkit.pattern.discovery'); 50 | } 51 | 52 | /** 53 | * Test basic component discovery. 54 | * 55 | * @covers ::discoverComponents 56 | */ 57 | public function testDiscoverComponents() { 58 | $module_path = $this->getModulePath('patternkit_example'); 59 | $component_path = $module_path . '/lib/patternkit/src'; 60 | $components = PatternLibraryParserBase::discoverComponents($component_path, ['json']); 61 | $expected_components = [ 62 | 'example' => 63 | [ 64 | 'json' => $module_path . '/lib/patternkit/src/atoms/example/src/example.json', 65 | ], 66 | 'example_filtered' => 67 | [ 68 | 'json' => $module_path . '/lib/patternkit/src/atoms/example_filtered/src/example_filtered.json', 69 | ], 70 | 'example_ref' => 71 | [ 72 | 'json' => $module_path . '/lib/patternkit/src/atoms/example_ref/src/example_ref.json', 73 | ], 74 | ]; 75 | 76 | $this->assertIsArray($components); 77 | $this->assertEquals($expected_components, $components); 78 | } 79 | 80 | /** 81 | * @covers ::createPattern 82 | */ 83 | public function testCreatePattern() { 84 | $this->markTestIncomplete('Test not yet implemented.'); 85 | } 86 | 87 | /** 88 | * Test asset fetching. 89 | * 90 | * @covers ::fetchAssets 91 | */ 92 | public function testFetchAssets() { 93 | $this->markTestIncomplete('Test not yet implemented.'); 94 | } 95 | 96 | /** 97 | * @covers ::fetchPatternAssets 98 | */ 99 | public function testFetchPatternAssets() { 100 | $this->markTestIncomplete('Test not yet implemented.'); 101 | } 102 | 103 | /** 104 | * @covers ::fetchSingleAsset 105 | */ 106 | public function testFetchSingleAsset() { 107 | $this->markTestIncomplete('Test not yet implemented.'); 108 | } 109 | 110 | /** 111 | * @covers ::fetchFragmentAssets 112 | */ 113 | public function testFetchFragmentAssets() { 114 | $this->markTestIncomplete('Test not yet implemented.'); 115 | } 116 | 117 | /** 118 | * @covers ::parsePatternLibraryInfo 119 | */ 120 | public function testParsePatternLibraryInfo() { 121 | $this->markTestIncomplete('Test not yet implemented.'); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /tests/src/Kernel/Loader/PatternLibraryLoaderTest.php: -------------------------------------------------------------------------------- 1 | container->get('twig.loader.patternlibrary'); 30 | $namespaces = $patternLoader->getNamespaces(); 31 | 32 | $this->assertIsArray($namespaces); 33 | $this->assertNotEmpty($namespaces, 'Pattern loader namespaces were not expected to be empty.'); 34 | $this->assertContains('patternkit', $namespaces, 'A "patternkit" namespace was expected for registration.'); 35 | $this->assertEquals( 36 | [$this->getModulePath('patternkit_example') . '/lib/patternkit/src'], 37 | $patternLoader->getPaths('patternkit'), 38 | 'The path registered for the "patternkit" namespace did not match expectations.'); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/src/Traits/JsonDecodeTrait.php: -------------------------------------------------------------------------------- 1 | getBlockManager()->createInstance($plugin_id); 61 | 62 | // Fake the form state to run submission handling. 63 | $form_state = new FormState(); 64 | $form_state->setValues([ 65 | 'instance_config' => $config, 66 | 'label' => '', 67 | 'reusable' => FALSE, 68 | 'presentation_style' => 'html', 69 | 'version' => NULL, 70 | ]); 71 | $block->blockSubmit([], $form_state); 72 | 73 | return $block; 74 | } 75 | 76 | /** 77 | * Place a block in an entity layout with layout builder enabled. 78 | * 79 | * @param \Drupal\Core\Entity\EntityInterface $entity 80 | * The entity to place the block into the layout display of. 81 | * @param \Drupal\patternkit\Plugin\Block\PatternkitBlock $block 82 | * The block instance to place in the layout. 83 | */ 84 | protected function placePatternBlockInLayout(EntityInterface $entity, BlockPatternkitBlock $block): void { 85 | $storage = $this->getSectionStorageForEntity($entity); 86 | $section = $storage->getSection(0); 87 | 88 | $config = $block->getConfiguration(); 89 | unset($config['instance_uuid']); 90 | $component = new SectionComponent($entity->uuid(), 'content', $config); 91 | $section->appendComponent($component); 92 | 93 | $storage->save(); 94 | } 95 | 96 | /** 97 | * Get the block manager service. 98 | * 99 | * @return \Drupal\Core\Block\BlockManagerInterface 100 | * The block manager service. 101 | */ 102 | private function getBlockManager(): BlockManagerInterface { 103 | return $this->blockManager ?: $this->blockManager = \Drupal::service('plugin.manager.block'); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /tests/src/Traits/SchemaFixtureTrait.php: -------------------------------------------------------------------------------- 1 | pathMap[$type][$name]; 28 | } 29 | 30 | /** 31 | * Set a map of return values for drupalGetPath based on passed arguments. 32 | * 33 | * @param array $map 34 | * A two-dimensional array of return values keyed by: 35 | * - $type 36 | * - $name 37 | * 38 | * @return void 39 | */ 40 | public function setPathMapping(array $map): void { 41 | $this->pathMap = $map; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/Unit/Schema/TestPatternkitRefProvider.php: -------------------------------------------------------------------------------- 1 | patternSchemas = []; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getPatternSchema(string $asset_id): string { 33 | if (isset($this->patternSchemas[$asset_id])) { 34 | return $this->patternSchemas[$asset_id]; 35 | } 36 | else { 37 | throw new SchemaReferenceException('Unknown component referenced in pattern schema: ' . $asset_id); 38 | } 39 | } 40 | 41 | /** 42 | * Register an asset and related schema for loading during tests. 43 | * 44 | * @param string $asset_id 45 | * The namespaced asset identifier used when loading a pattern. 46 | * @param string $schema_json 47 | * The schema JSON string to return for this pattern. 48 | */ 49 | public function registerPatternSchema(string $asset_id, string $schema_json): void { 50 | $this->patternSchemas[$asset_id] = $schema_json; 51 | } 52 | 53 | } 54 | --------------------------------------------------------------------------------